diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 26fbbfcdcd..c05b0d01a9 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -881,6 +881,12 @@ class SPANDATA: Example: "tcp", "udp", "unix" """ + PROCESS_PID = "process.pid" + """ + The process ID of the running process. + Example: 12345 + """ + PROFILER_ID = "profiler_id" """ Label identifying the profiler id that the span occurred in. This should be a string. @@ -924,6 +930,24 @@ class SPANDATA: Example: "MainThread" """ + 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_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. diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index e3120a3b32..5d8df43eb2 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -8,10 +8,13 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.tracing import Span +from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import ( EnvironHeaders, should_propagate_trace, add_http_request_source, + has_span_streaming_enabled, ) from sentry_sdk.utils import ( SENSITIVE_DATA_SUBSTITUTE, @@ -31,6 +34,7 @@ from typing import Dict from typing import Optional from typing import List + from typing import Union from sentry_sdk._types import Event, Hint @@ -99,22 +103,46 @@ def putrequest( with capture_internal_exceptions(): parsed_url = parse_url(real_url, sanitize=False) - 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("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + 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: + 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) + + 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("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + set_on_span = span.set_data # for proxies, these point to the proxy host/port if tunnel_host: - span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, self.host) - span.set_data(SPANDATA.NETWORK_PEER_PORT, self.port) + 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) @@ -145,13 +173,23 @@ def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": try: rv = real_getresponse(self, *args, **kwargs) - span.set_http_status(int(rv.status)) - span.set_data("reason", rv.reason) + 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) finally: - span.finish() + 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) + with capture_internal_exceptions(): + add_http_request_source(span) return rv @@ -226,11 +264,24 @@ def sentry_patched_popen_init( env = None - with sentry_sdk.start_span( - op=OP.SUBPROCESS, - name=description, - origin="auto.subprocess.stdlib.subprocess", - ) as span: + 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 ): @@ -244,12 +295,16 @@ def sentry_patched_popen_init( ) env["SUBPROCESS_" + k.upper().replace("-", "_")] = v - if cwd: + if cwd and isinstance(span, Span): span.set_data("subprocess.cwd", cwd) rv = old_popen_init(self, *a, **kw) - span.set_tag("subprocess.pid", self.pid) + 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 @@ -260,12 +315,24 @@ def sentry_patched_popen_init( def sentry_patched_popen_wait( self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" ) -> "Any": - 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) + 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 @@ -275,12 +342,24 @@ def sentry_patched_popen_wait( def sentry_patched_popen_communicate( self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" ) -> "Any": - 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) + 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 diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 6f962d6ad9..33aa95825d 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -145,13 +145,20 @@ def before_breadcrumb(crumb, hint): ) -def test_empty_realurl(sentry_init): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_empty_realurl( + sentry_init, + span_streaming, +): """ Ensure that after using sentry_sdk.init you can putrequest a None url. """ - sentry_init(dsn="") + sentry_init( + dsn="", + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) HTTPConnection("localhost", port=PORT).putrequest("POST", None) @@ -204,11 +211,19 @@ def test_httplib_misuse(sentry_init, capture_events, request): ) -def test_outgoing_trace_headers(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_outgoing_trace_headers( + sentry_init, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) already_patched_getresponse = HTTPSConnection.getresponse - request_headers = {} class HTTPSConnectionRecordingRequestHeaders(HTTPSConnection): @@ -238,25 +253,49 @@ def getresponse(self, *args, **kwargs): ), } - transaction = continue_trace(headers) - - with start_transaction( - transaction=transaction, - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - trace_id="12312012123120121231201212312012", - ) as transaction: - connection = HTTPSConnectionRecordingRequestHeaders("localhost", port=PORT) - connection.request("GET", "/top-chasers") - connection.getresponse() + if span_streaming: + items = capture_items("span") + sentry_sdk.traces.continue_trace(headers) + + with sentry_sdk.traces.start_span( + name="/interactions/other-dogs/new-dog", + attributes={ + "sentry.op": "greeting.sniff", + }, + ): + connection = HTTPSConnectionRecordingRequestHeaders("localhost", port=PORT) + connection.request("GET", "/top-chasers") + connection.getresponse() + + sentry_sdk.flush() + request_span = next(item.payload for item in items if item.type == "span") + expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format( + trace_id=request_span["trace_id"], + parent_span_id=request_span["span_id"], + sampled=1, + ) + else: + events = capture_events() + transaction = continue_trace(headers) + + with start_transaction( + transaction=transaction, + name="/interactions/other-dogs/new-dog", + op="greeting.sniff", + trace_id="12312012123120121231201212312012", + ) as transaction: + connection = HTTPSConnectionRecordingRequestHeaders("localhost", port=PORT) + connection.request("GET", "/top-chasers") + connection.getresponse() + + (event,) = events + request_span = event["spans"][-1] + expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format( + trace_id=event["contexts"]["trace"]["trace_id"], + parent_span_id=request_span["span_id"], + sampled=1, + ) - (event,) = events - request_span = event["spans"][-1] - expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format( - trace_id=event["contexts"]["trace"]["trace_id"], - parent_span_id=request_span["span_id"], - sampled=1, - ) assert request_headers["sentry-trace"] == expected_sentry_trace expected_outgoing_baggage = ( @@ -270,11 +309,20 @@ def getresponse(self, *args, **kwargs): assert request_headers["baggage"] == expected_outgoing_baggage -def test_outgoing_trace_headers_head_sdk(sentry_init, capture_events): - sentry_init(traces_sample_rate=0.5, release="foo") +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_outgoing_trace_headers_head_sdk( + sentry_init, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + traces_sample_rate=0.5, + release="foo", + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) already_patched_getresponse = HTTPSConnection.getresponse - request_headers = {} class HTTPSConnectionRecordingRequestHeaders(HTTPSConnection): @@ -293,35 +341,71 @@ def send(self, *args, **kwargs) -> None: def getresponse(self, *args, **kwargs): return already_patched_getresponse(self, *args, **kwargs) - events = capture_events() - - with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=250000): - transaction = continue_trace({}) + if span_streaming: + items = capture_items("span") + + sentry_sdk.traces.continue_trace({}) + + with mock.patch( + "sentry_sdk.tracing_utils.Random.randrange", return_value=250000 + ): + with sentry_sdk.traces.start_span(name="Head SDK tx"): + connection = HTTPSConnectionRecordingRequestHeaders( + "localhost", port=PORT + ) + connection.request("GET", "/top-chasers") + connection.getresponse() + + sentry_sdk.flush() + request_span = next(item.payload for item in items if item.type == "span") + expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format( + trace_id=request_span["trace_id"], + parent_span_id=request_span["span_id"], + sampled=1, + ) - with start_transaction(transaction=transaction, name="Head SDK tx") as transaction: - connection = HTTPSConnectionRecordingRequestHeaders("localhost", port=PORT) - connection.request("GET", "/top-chasers") - connection.getresponse() + expected_outgoing_baggage = ( + "sentry-trace_id=%s," + "sentry-sample_rand=0.250000," + "sentry-environment=production," + "sentry-release=foo," + "sentry-transaction=Head%%20SDK%%20tx," + "sentry-sample_rate=0.5," + "sentry-sampled=true" + ) % request_span["trace_id"] + else: + events = capture_events() + + with mock.patch( + "sentry_sdk.tracing_utils.Random.randrange", return_value=250000 + ): + transaction = continue_trace({}) + + with start_transaction( + transaction=transaction, name="Head SDK tx" + ) as transaction: + connection = HTTPSConnectionRecordingRequestHeaders("localhost", port=PORT) + connection.request("GET", "/top-chasers") + connection.getresponse() + + (event,) = events + request_span = event["spans"][-1] + expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format( + trace_id=event["contexts"]["trace"]["trace_id"], + parent_span_id=request_span["span_id"], + sampled=1, + ) - (event,) = events - request_span = event["spans"][-1] - expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format( - trace_id=event["contexts"]["trace"]["trace_id"], - parent_span_id=request_span["span_id"], - sampled=1, - ) + expected_outgoing_baggage = ( + "sentry-trace_id=%s," + "sentry-sample_rand=0.250000," + "sentry-environment=production," + "sentry-release=foo," + "sentry-sample_rate=0.5," + "sentry-sampled=%s" + ) % (transaction.trace_id, "true" if transaction.sampled else "false") assert request_headers["sentry-trace"] == expected_sentry_trace - - expected_outgoing_baggage = ( - "sentry-trace_id=%s," - "sentry-sample_rand=0.250000," - "sentry-environment=production," - "sentry-release=foo," - "sentry-sample_rate=0.5," - "sentry-sampled=%s" - ) % (transaction.trace_id, "true" if transaction.sampled else "false") - assert request_headers["baggage"] == expected_outgoing_baggage @@ -384,12 +468,19 @@ def getresponse(self, *args, **kwargs): ], ], ) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_option_trace_propagation_targets( - sentry_init, trace_propagation_targets, host, path, trace_propagated + sentry_init, + trace_propagation_targets, + host, + path, + trace_propagated, + span_streaming, ): sentry_init( trace_propagation_targets=trace_propagation_targets, traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) already_patched_getresponse = HTTPSConnection.getresponse @@ -419,17 +510,30 @@ def getresponse(self, *args, **kwargs): ) } - transaction = continue_trace(headers) - - with start_transaction( - transaction=transaction, - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - trace_id="12312012123120121231201212312012", - ) as transaction: - connection = HTTPSConnectionRecordingRequestHeaders(host) - connection.request("GET", path) - connection.getresponse() + if span_streaming: + sentry_sdk.traces.continue_trace(headers) + + with sentry_sdk.traces.start_span( + name="/interactions/other-dogs/new-dog", + attributes={ + "sentry.op": "greeting.sniff", + }, + ): + connection = HTTPSConnectionRecordingRequestHeaders(host) + connection.request("GET", path) + connection.getresponse() + else: + transaction = continue_trace(headers) + + with start_transaction( + transaction=transaction, + name="/interactions/other-dogs/new-dog", + op="greeting.sniff", + trace_id="12312012123120121231201212312012", + ) as transaction: + connection = HTTPSConnectionRecordingRequestHeaders(host) + connection.request("GET", path) + connection.getresponse() if trace_propagated: assert "sentry-trace" in request_headers @@ -439,108 +543,215 @@ def getresponse(self, *args, **kwargs): assert "baggage" not in request_headers -def test_request_source_disabled(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_request_source_disabled( + sentry_init, + capture_events, + capture_items, + span_streaming, +): sentry_options = { "traces_sample_rate": 1.0, "enable_http_request_source": False, "http_request_source_threshold_ms": 0, } - sentry_init(**sentry_options) + sentry_init( + **sentry_options, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) - events = capture_events() + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() - with start_transaction(name="foo"): - conn = HTTPConnection("localhost", port=PORT) - conn.request("GET", "/foo") - conn.getresponse() + sentry_sdk.flush() + span = next(item.payload for item in items if item.type == "span") + assert span["name"].startswith("GET") - (event,) = events + attributes = span["attributes"] + + assert SPANDATA.CODE_LINE_NUMBER not in attributes + assert SPANDATA.CODE_NAMESPACE not in attributes + assert SPANDATA.CODE_FILE_PATH not in attributes + assert SPANDATA.CODE_FUNCTION not in attributes + else: + events = capture_events() - span = event["spans"][-1] - assert span["description"].startswith("GET") + with start_transaction(name="foo"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") - data = span.get("data", {}) + data = span.get("data", {}) - assert SPANDATA.CODE_LINENO not in data - assert SPANDATA.CODE_NAMESPACE not in data - assert SPANDATA.CODE_FILEPATH not in data - assert SPANDATA.CODE_FUNCTION not in data + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data @pytest.mark.parametrize("enable_http_request_source", [None, True]) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_request_source_enabled( - sentry_init, capture_events, enable_http_request_source + sentry_init, + capture_events, + capture_items, + enable_http_request_source, + span_streaming, ): sentry_options = { "traces_sample_rate": 1.0, "http_request_source_threshold_ms": 0, + "_experiments": {"trace_lifecycle": "stream" if span_streaming else "static"}, } + if enable_http_request_source is not None: sentry_options["enable_http_request_source"] = enable_http_request_source - sentry_init(**sentry_options) + if span_streaming: + sentry_init( + **sentry_options, + ) - events = capture_events() + items = capture_items("span") - with start_transaction(name="foo"): - conn = HTTPConnection("localhost", port=PORT) - conn.request("GET", "/foo") - conn.getresponse() + with sentry_sdk.traces.start_span(name="custom parent"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() - (event,) = events + sentry_sdk.flush() + span = next(item.payload for item in items if item.type == "span") + assert span["name"].startswith("GET") + + attributes = span["attributes"] + + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes + else: + sentry_init(**sentry_options) - span = event["spans"][-1] - assert span["description"].startswith("GET") + events = capture_events() + + with start_transaction(name="foo"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() - data = span.get("data", {}) + (event,) = events - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data + span = event["spans"][-1] + assert span["description"].startswith("GET") + data = span.get("data", {}) -def test_request_source(sentry_init, capture_events): + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_request_source( + sentry_init, + capture_events, + capture_items, + span_streaming, +): sentry_init( traces_sample_rate=1.0, enable_http_request_source=True, http_request_source_threshold_ms=0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) + if span_streaming: + items = capture_items("span") - events = capture_events() + with sentry_sdk.traces.start_span(name="custom parent"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() - with start_transaction(name="foo"): - conn = HTTPConnection("localhost", port=PORT) - conn.request("GET", "/foo") - conn.getresponse() + sentry_sdk.flush() + span = next(item.payload for item in items if item.type == "span") + assert span["name"].startswith("GET") - (event,) = events + attributes = span["attributes"] + + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes + + assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int + assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0 + assert ( + attributes.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.stdlib.test_httplib" + ) + assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith( + "tests/integrations/stdlib/test_httplib.py" + ) - span = event["spans"][-1] - assert span["description"].startswith("GET") + is_relative_path = attributes.get(SPANDATA.CODE_FILE_PATH)[0] != os.sep + assert is_relative_path - data = span.get("data", {}) + assert attributes.get(SPANDATA.CODE_FUNCTION) == "test_request_source" + else: + events = capture_events() - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data + with start_transaction(name="foo"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.stdlib.test_httplib" - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/stdlib/test_httplib.py" - ) + (event,) = events - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path + span = event["spans"][-1] + assert span["description"].startswith("GET") - assert data.get(SPANDATA.CODE_FUNCTION) == "test_request_source" + data = span.get("data", {}) + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data -def test_request_source_with_module_in_search_path(sentry_init, capture_events): + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.stdlib.test_httplib" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/stdlib/test_httplib.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "test_request_source" + + +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_request_source_with_module_in_search_path( + sentry_init, + capture_events, + capture_items, + span_streaming, +): """ Test that request source is relative to the path of the module it ran in """ @@ -548,186 +759,405 @@ def test_request_source_with_module_in_search_path(sentry_init, capture_events): traces_sample_rate=1.0, enable_http_request_source=True, http_request_source_threshold_ms=0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) + if span_streaming: + items = capture_items("span") - events = capture_events() + with sentry_sdk.traces.start_span(name="custom parent"): + from httplib_helpers.helpers import get_request_with_connection - with start_transaction(name="foo"): - from httplib_helpers.helpers import get_request_with_connection + conn = HTTPConnection("localhost", port=PORT) + get_request_with_connection(conn, "/foo") - conn = HTTPConnection("localhost", port=PORT) - get_request_with_connection(conn, "/foo") + sentry_sdk.flush() + span = next(item.payload for item in items if item.type == "span") + assert span["name"].startswith("GET") - (event,) = events + attributes = span["attributes"] - span = event["spans"][-1] - assert span["description"].startswith("GET") + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes - data = span.get("data", {}) + assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int + assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0 + assert attributes.get(SPANDATA.CODE_NAMESPACE) == "httplib_helpers.helpers" + assert attributes.get(SPANDATA.CODE_FILE_PATH) == "httplib_helpers/helpers.py" - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data + is_relative_path = attributes.get(SPANDATA.CODE_FILE_PATH)[0] != os.sep + assert is_relative_path - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "httplib_helpers.helpers" - assert data.get(SPANDATA.CODE_FILEPATH) == "httplib_helpers/helpers.py" + assert attributes.get(SPANDATA.CODE_FUNCTION) == "get_request_with_connection" + else: + events = capture_events() - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path + with start_transaction(name="foo"): + from httplib_helpers.helpers import get_request_with_connection - assert data.get(SPANDATA.CODE_FUNCTION) == "get_request_with_connection" + conn = HTTPConnection("localhost", port=PORT) + get_request_with_connection(conn, "/foo") + (event,) = events -def test_no_request_source_if_duration_too_short(sentry_init, capture_events): + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert data.get(SPANDATA.CODE_NAMESPACE) == "httplib_helpers.helpers" + assert data.get(SPANDATA.CODE_FILEPATH) == "httplib_helpers/helpers.py" + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "get_request_with_connection" + + +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_no_request_source_if_duration_too_short( + sentry_init, + capture_events, + capture_items, + span_streaming, +): sentry_init( traces_sample_rate=1.0, enable_http_request_source=True, http_request_source_threshold_ms=100, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) add_http_request_source = sentry_sdk.tracing_utils.add_http_request_source def add_http_request_source_with_pinned_timestamps(span): - span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) - span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999) - return add_http_request_source(span) - - events = capture_events() + if span_streaming: + span._start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span._timestamp = datetime.datetime(2024, 1, 1, microsecond=99999) + result = add_http_request_source(span) + span._timestamp = None + return result + else: + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999) + return add_http_request_source(span) + + if span_streaming: + items = capture_items("span") + with mock.patch( + "sentry_sdk.integrations.stdlib.add_http_request_source", + add_http_request_source_with_pinned_timestamps, + ): + with sentry_sdk.traces.start_span(name="foo"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() + + sentry_sdk.flush() + span = next(item.payload for item in items if item.type == "span") + assert span["name"].startswith("GET") + + attributes = span["attributes"] + + assert SPANDATA.CODE_LINE_NUMBER not in attributes + assert SPANDATA.CODE_NAMESPACE not in attributes + assert SPANDATA.CODE_FILE_PATH not in attributes + assert SPANDATA.CODE_FUNCTION not in attributes + else: + events = capture_events() - with mock.patch( - "sentry_sdk.integrations.stdlib.add_http_request_source", - add_http_request_source_with_pinned_timestamps, - ): - with start_transaction(name="foo"): - conn = HTTPConnection("localhost", port=PORT) - conn.request("GET", "/foo") - conn.getresponse() + with mock.patch( + "sentry_sdk.integrations.stdlib.add_http_request_source", + add_http_request_source_with_pinned_timestamps, + ): + with start_transaction(name="foo"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() - (event,) = events + (event,) = events - span = event["spans"][-1] - assert span["description"].startswith("GET") + span = event["spans"][-1] + assert span["description"].startswith("GET") - data = span.get("data", {}) + data = span.get("data", {}) - assert SPANDATA.CODE_LINENO not in data - assert SPANDATA.CODE_NAMESPACE not in data - assert SPANDATA.CODE_FILEPATH not in data - assert SPANDATA.CODE_FUNCTION not in data + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data -def test_request_source_if_duration_over_threshold(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_request_source_if_duration_over_threshold( + sentry_init, + capture_events, + capture_items, + span_streaming, +): sentry_init( traces_sample_rate=1.0, enable_http_request_source=True, http_request_source_threshold_ms=100, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) add_http_request_source = sentry_sdk.tracing_utils.add_http_request_source def add_http_request_source_with_pinned_timestamps(span): - span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) - span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001) - return add_http_request_source(span) - - events = capture_events() + if span_streaming: + span._start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span._timestamp = datetime.datetime(2024, 1, 1, microsecond=100001) + result = add_http_request_source(span) + span._timestamp = None + return result + else: + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001) + return add_http_request_source(span) + + if span_streaming: + items = capture_items("span") + + with mock.patch( + "sentry_sdk.integrations.stdlib.add_http_request_source", + add_http_request_source_with_pinned_timestamps, + ): + with sentry_sdk.traces.start_span(name="foo"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() + + sentry_sdk.flush() + span = next(item.payload for item in items if item.type == "span") + assert span["name"].startswith("GET") + + attributes = span["attributes"] + + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes + + assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int + assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0 + assert ( + attributes.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.stdlib.test_httplib" + ) + assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith( + "tests/integrations/stdlib/test_httplib.py" + ) - with mock.patch( - "sentry_sdk.integrations.stdlib.add_http_request_source", - add_http_request_source_with_pinned_timestamps, - ): - with start_transaction(name="foo"): - conn = HTTPConnection("localhost", port=PORT) - conn.request("GET", "/foo") - conn.getresponse() + is_relative_path = attributes.get(SPANDATA.CODE_FILE_PATH)[0] != os.sep + assert is_relative_path - (event,) = events + assert ( + attributes.get(SPANDATA.CODE_FUNCTION) + == "add_http_request_source_with_pinned_timestamps" + ) + else: + events = capture_events() + + with mock.patch( + "sentry_sdk.integrations.stdlib.add_http_request_source", + add_http_request_source_with_pinned_timestamps, + ): + with start_transaction(name="foo"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.stdlib.test_httplib" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/stdlib/test_httplib.py" + ) - span = event["spans"][-1] - assert span["description"].startswith("GET") + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path - data = span.get("data", {}) + assert ( + data.get(SPANDATA.CODE_FUNCTION) + == "add_http_request_source_with_pinned_timestamps" + ) - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.stdlib.test_httplib" - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/stdlib/test_httplib.py" +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_span_origin( + sentry_init, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + traces_sample_rate=1.0, + debug=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path + if span_streaming: + items = capture_items("span") - assert ( - data.get(SPANDATA.CODE_FUNCTION) - == "add_http_request_source_with_pinned_timestamps" - ) + with sentry_sdk.traces.start_span(name="custom parent"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert spans[1]["attributes"]["sentry.origin"] == "manual" -def test_span_origin(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0, debug=True) - events = capture_events() + assert spans[0]["attributes"]["sentry.op"] == "http.client" + assert spans[0]["attributes"]["sentry.origin"] == "auto.http.stdlib.httplib" + else: + events = capture_events() - with start_transaction(name="foo"): - conn = HTTPConnection("localhost", port=PORT) - conn.request("GET", "/foo") - conn.getresponse() + with start_transaction(name="foo"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/foo") + conn.getresponse() - (event,) = events - assert event["contexts"]["trace"]["origin"] == "manual" + (event,) = events + assert event["contexts"]["trace"]["origin"] == "manual" - assert event["spans"][0]["op"] == "http.client" - assert event["spans"][0]["origin"] == "auto.http.stdlib.httplib" + assert event["spans"][0]["op"] == "http.client" + assert event["spans"][0]["origin"] == "auto.http.stdlib.httplib" -def test_http_timeout(monkeypatch, sentry_init, capture_envelopes): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_http_timeout( + monkeypatch, + sentry_init, + capture_envelopes, + capture_items, + span_streaming, +): mock_readinto = mock.Mock(side_effect=TimeoutError) monkeypatch.setattr(SocketIO, "readinto", mock_readinto) - sentry_init(traces_sample_rate=1.0) + sentry_init( + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) - envelopes = capture_envelopes() + if span_streaming: + items = capture_items("span") + + with pytest.raises(TimeoutError): + with sentry_sdk.traces.start_span( + name="name", + attributes={ + "sentry.op": "op", + }, + ): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/bla") + conn.getresponse() + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 2 + span = spans[0] + assert span["attributes"]["sentry.op"] == "http.client" + assert span["name"] == f"GET http://localhost:{PORT}/bla" # noqa: E231 + else: + envelopes = capture_envelopes() - with pytest.raises(TimeoutError): - with start_transaction(op="op", name="name"): - conn = HTTPConnection("localhost", port=PORT) - conn.request("GET", "/bla") - conn.getresponse() + with pytest.raises(TimeoutError): + with start_transaction(op="op", name="name"): + conn = HTTPConnection("localhost", port=PORT) + conn.request("GET", "/bla") + conn.getresponse() - (transaction_envelope,) = envelopes - transaction = transaction_envelope.get_transaction_event() - assert len(transaction["spans"]) == 1 + (transaction_envelope,) = envelopes + transaction = transaction_envelope.get_transaction_event() + assert len(transaction["spans"]) == 1 - span = transaction["spans"][0] - assert span["op"] == "http.client" - assert span["description"] == f"GET http://localhost:{PORT}/bla" # noqa: E231 + span = transaction["spans"][0] + assert span["op"] == "http.client" + assert span["description"] == f"GET http://localhost:{PORT}/bla" # noqa: E231 @pytest.mark.parametrize("tunnel_port", [8080, None]) -def test_proxy_http_tunnel(sentry_init, capture_events, tunnel_port): - sentry_init(traces_sample_rate=1.0) - events = capture_events() +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_proxy_http_tunnel( + sentry_init, + capture_events, + capture_items, + tunnel_port, + span_streaming, +): + sentry_init( + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) - with start_transaction(name="test_transaction"): - conn = HTTPConnection("localhost", PROXY_PORT) - conn.set_tunnel("api.example.com", tunnel_port) - conn.request("GET", "/foo") - conn.getresponse() + if span_streaming: + items = capture_items("span") - (event,) = events - (span,) = event["spans"] - - port_modifier = f":{tunnel_port}" if tunnel_port else "" - assert span["description"] == f"GET http://api.example.com{port_modifier}/foo" - assert span["data"]["url"] == f"http://api.example.com{port_modifier}/foo" - assert span["data"][SPANDATA.HTTP_METHOD] == "GET" - assert span["data"][SPANDATA.NETWORK_PEER_ADDRESS] == "localhost" - assert span["data"][SPANDATA.NETWORK_PEER_PORT] == PROXY_PORT + with sentry_sdk.traces.start_span(name="custom parent"): + conn = HTTPConnection("localhost", PROXY_PORT) + conn.set_tunnel("api.example.com", tunnel_port) + conn.request("GET", "/foo") + conn.getresponse() + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + (span,) = ( + span + for span in spans + if span["attributes"].get("sentry.origin") == "auto.http.stdlib.httplib" + ) + + port_modifier = f":{tunnel_port}" if tunnel_port else "" + assert span["name"] == f"GET http://api.example.com{port_modifier}/foo" + assert ( + span["attributes"][SPANDATA.URL_FULL] + == f"http://api.example.com{port_modifier}/foo" + ) + assert span["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "GET" + assert span["attributes"][SPANDATA.NETWORK_PEER_ADDRESS] == "localhost" + assert span["attributes"][SPANDATA.NETWORK_PEER_PORT] == PROXY_PORT + else: + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = HTTPConnection("localhost", PROXY_PORT) + conn.set_tunnel("api.example.com", tunnel_port) + conn.request("GET", "/foo") + conn.getresponse() + + (event,) = events + (span,) = event["spans"] + + port_modifier = f":{tunnel_port}" if tunnel_port else "" + assert span["description"] == f"GET http://api.example.com{port_modifier}/foo" + assert span["data"]["url"] == f"http://api.example.com{port_modifier}/foo" + assert span["data"][SPANDATA.HTTP_METHOD] == "GET" + assert span["data"][SPANDATA.NETWORK_PEER_ADDRESS] == "localhost" + assert span["data"][SPANDATA.NETWORK_PEER_PORT] == PROXY_PORT diff --git a/tests/integrations/stdlib/test_subprocess.py b/tests/integrations/stdlib/test_subprocess.py index 593ef8a0dc..8938db4211 100644 --- a/tests/integrations/stdlib/test_subprocess.py +++ b/tests/integrations/stdlib/test_subprocess.py @@ -1,4 +1,5 @@ import os +import sentry_sdk import platform import subprocess import sys @@ -7,6 +8,7 @@ import pytest from sentry_sdk import capture_message, start_transaction +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.stdlib import StdlibIntegration from tests.conftest import ApproxDict @@ -42,153 +44,291 @@ def __len__(self): ) @pytest.mark.parametrize("env_mapping", [None, os.environ, ImmutableDict(os.environ)]) @pytest.mark.parametrize("with_cwd", [True, False]) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_subprocess_basic( sentry_init, capture_events, + capture_items, monkeypatch, positional_args, iterator, env_mapping, with_cwd, + span_streaming, ): monkeypatch.setenv("FOO", "bar") old_environ = dict(os.environ) - sentry_init(integrations=[StdlibIntegration()], traces_sample_rate=1.0) - events = capture_events() - - with start_transaction(name="foo") as transaction: - args = [ - sys.executable, - "-c", - "import os; " - "import sentry_sdk; " - "from sentry_sdk.integrations.stdlib import get_subprocess_traceparent_headers; " - "sentry_sdk.init(); " - "assert os.environ['FOO'] == 'bar'; " - "print(dict(get_subprocess_traceparent_headers()))", - ] + sentry_init( + integrations=[StdlibIntegration()], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + if span_streaming: + items = capture_items("event", "transaction", "span") + + with sentry_sdk.traces.start_span(name="custom parent") as span: + args = [ + sys.executable, + "-c", + "import os; " + "import sentry_sdk; " + "from sentry_sdk.integrations.stdlib import get_subprocess_traceparent_headers; " + "sentry_sdk.init(); " + "assert os.environ['FOO'] == 'bar'; " + "print(dict(get_subprocess_traceparent_headers()))", + ] + + if iterator: + args = iter(args) + + if positional_args: + a = ( + args, + 0, # bufsize + None, # executable + None, # stdin + subprocess.PIPE, # stdout + None, # stderr + None, # preexec_fn + False, # close_fds + False, # shell + os.getcwd() if with_cwd else None, # cwd + ) + + if env_mapping is not None: + a += (env_mapping,) + + popen = subprocess.Popen(*a) + + else: + kw = {"args": args, "stdout": subprocess.PIPE} + + if with_cwd: + kw["cwd"] = os.getcwd() + + if env_mapping is not None: + kw["env"] = env_mapping + + popen = subprocess.Popen(**kw) + + output, unused_err = popen.communicate() + retcode = popen.poll() + assert not retcode + + assert os.environ == old_environ + + assert span.trace_id in str(output) + + capture_message("hi") + + (message_event,) = (item.payload for item in items if item.type == "event") + assert message_event["message"] == "hi" + + data = ApproxDict({}) + + sentry_sdk.flush() + ( + subprocess_init_span, + subprocess_wait_span, + subprocess_communicate_span, + parent_span, + ) = (item.payload for item in items if item.type == "span") + + assert ( + subprocess_init_span["attributes"]["sentry.op"], + subprocess_wait_span["attributes"]["sentry.op"], + subprocess_communicate_span["attributes"]["sentry.op"], + ) == ("subprocess", "subprocess.wait", "subprocess.communicate") + + # span hierarchy + assert ( + subprocess_wait_span["parent_span_id"] + == subprocess_communicate_span["span_id"] + ) + + assert ( + subprocess_communicate_span["parent_span_id"] + == subprocess_init_span["parent_span_id"] + == parent_span["span_id"] + ) + + assert ( + subprocess_init_span["attributes"][SPANDATA.PROCESS_PID] + == subprocess_wait_span["attributes"][SPANDATA.PROCESS_PID] + == subprocess_communicate_span["attributes"][SPANDATA.PROCESS_PID] + ) + + # data of init span + assert subprocess_init_span.get("attributes", {}) == data if iterator: - args = iter(args) - - if positional_args: - a = ( - args, - 0, # bufsize - None, # executable - None, # stdin - subprocess.PIPE, # stdout - None, # stderr - None, # preexec_fn - False, # close_fds - False, # shell - os.getcwd() if with_cwd else None, # cwd - ) - - if env_mapping is not None: - a += (env_mapping,) - - popen = subprocess.Popen(*a) - + assert "iterator" in subprocess_init_span["name"] + assert subprocess_init_span["name"].startswith("<") else: - kw = {"args": args, "stdout": subprocess.PIPE} - - if with_cwd: - kw["cwd"] = os.getcwd() - - if env_mapping is not None: - kw["env"] = env_mapping - - popen = subprocess.Popen(**kw) - - output, unused_err = popen.communicate() - retcode = popen.poll() - assert not retcode - - assert os.environ == old_environ - - assert transaction.trace_id in str(output) - - capture_message("hi") + assert sys.executable + " -c" in subprocess_init_span["name"] - ( - transaction_event, - message_event, - ) = events - - assert message_event["message"] == "hi" - - data = ApproxDict({"subprocess.cwd": os.getcwd()} if with_cwd else {}) - - (crumb,) = message_event["breadcrumbs"]["values"] - assert crumb == { - "category": "subprocess", - "data": data, - "message": crumb["message"], - "timestamp": crumb["timestamp"], - "type": "subprocess", - } - - if not iterator: - assert crumb["message"].startswith(sys.executable + " ") - - assert transaction_event["type"] == "transaction" - - ( - subprocess_init_span, - subprocess_communicate_span, - subprocess_wait_span, - ) = transaction_event["spans"] - - assert ( - subprocess_init_span["op"], - subprocess_communicate_span["op"], - subprocess_wait_span["op"], - ) == ("subprocess", "subprocess.communicate", "subprocess.wait") + else: + events = capture_events() + + with start_transaction(name="foo") as transaction: + args = [ + sys.executable, + "-c", + "import os; " + "import sentry_sdk; " + "from sentry_sdk.integrations.stdlib import get_subprocess_traceparent_headers; " + "sentry_sdk.init(); " + "assert os.environ['FOO'] == 'bar'; " + "print(dict(get_subprocess_traceparent_headers()))", + ] + + if iterator: + args = iter(args) + + if positional_args: + a = ( + args, + 0, # bufsize + None, # executable + None, # stdin + subprocess.PIPE, # stdout + None, # stderr + None, # preexec_fn + False, # close_fds + False, # shell + os.getcwd() if with_cwd else None, # cwd + ) + + if env_mapping is not None: + a += (env_mapping,) + + popen = subprocess.Popen(*a) + + else: + kw = {"args": args, "stdout": subprocess.PIPE} + + if with_cwd: + kw["cwd"] = os.getcwd() + + if env_mapping is not None: + kw["env"] = env_mapping + + popen = subprocess.Popen(**kw) + + output, unused_err = popen.communicate() + retcode = popen.poll() + assert not retcode + + assert os.environ == old_environ + + assert transaction.trace_id in str(output) + + capture_message("hi") + + ( + transaction_event, + message_event, + ) = events + + assert message_event["message"] == "hi" + + data = ApproxDict({"subprocess.cwd": os.getcwd()} if with_cwd else {}) + + (crumb,) = message_event["breadcrumbs"]["values"] + assert crumb == { + "category": "subprocess", + "data": data, + "message": crumb["message"], + "timestamp": crumb["timestamp"], + "type": "subprocess", + } + + if not iterator: + assert crumb["message"].startswith(sys.executable + " ") + + assert transaction_event["type"] == "transaction" + + ( + subprocess_init_span, + subprocess_communicate_span, + subprocess_wait_span, + ) = transaction_event["spans"] + + assert ( + subprocess_init_span["op"], + subprocess_communicate_span["op"], + subprocess_wait_span["op"], + ) == ("subprocess", "subprocess.communicate", "subprocess.wait") + + assert ( + subprocess_wait_span["parent_span_id"] + == subprocess_communicate_span["span_id"] + ) + + assert ( + subprocess_communicate_span["parent_span_id"] + == subprocess_init_span["parent_span_id"] + == transaction_event["contexts"]["trace"]["span_id"] + ) + + assert ( + subprocess_init_span["tags"]["subprocess.pid"] + == subprocess_wait_span["tags"]["subprocess.pid"] + == subprocess_communicate_span["tags"]["subprocess.pid"] + ) + + # data of init span + assert subprocess_init_span.get("data", {}) == data + if iterator: + assert "iterator" in subprocess_init_span["description"] + assert subprocess_init_span["description"].startswith("<") + else: + assert sys.executable + " -c" in subprocess_init_span["description"] - # span hierarchy - assert ( - subprocess_wait_span["parent_span_id"] == subprocess_communicate_span["span_id"] - ) - assert ( - subprocess_communicate_span["parent_span_id"] - == subprocess_init_span["parent_span_id"] - == transaction_event["contexts"]["trace"]["span_id"] - ) - # common data - assert ( - subprocess_init_span["tags"]["subprocess.pid"] - == subprocess_wait_span["tags"]["subprocess.pid"] - == subprocess_communicate_span["tags"]["subprocess.pid"] +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_subprocess_empty_env( + sentry_init, + monkeypatch, + span_streaming, +): + monkeypatch.setenv("TEST_MARKER", "should_not_be_seen") + sentry_init( + integrations=[StdlibIntegration()], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - - # data of init span - assert subprocess_init_span.get("data", {}) == data - if iterator: - assert "iterator" in subprocess_init_span["description"] - assert subprocess_init_span["description"].startswith("<") + if span_streaming: + with sentry_sdk.traces.start_span(name="custom parent"): + args = [ + sys.executable, + "-c", + "import os; print(os.environ.get('TEST_MARKER', None))", + ] + output = subprocess.check_output(args, env={}, universal_newlines=True) else: - assert sys.executable + " -c" in subprocess_init_span["description"] + with start_transaction(name="foo"): + args = [ + sys.executable, + "-c", + "import os; print(os.environ.get('TEST_MARKER', None))", + ] + output = subprocess.check_output(args, env={}, universal_newlines=True) - -def test_subprocess_empty_env(sentry_init, monkeypatch): - monkeypatch.setenv("TEST_MARKER", "should_not_be_seen") - sentry_init(integrations=[StdlibIntegration()], traces_sample_rate=1.0) - with start_transaction(name="foo"): - args = [ - sys.executable, - "-c", - "import os; print(os.environ.get('TEST_MARKER', None))", - ] - output = subprocess.check_output(args, env={}, universal_newlines=True) assert "should_not_be_seen" not in output -def test_subprocess_invalid_args(sentry_init): - sentry_init(integrations=[StdlibIntegration()]) +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_subprocess_invalid_args( + sentry_init, + span_streaming, +): + sentry_init( + integrations=[StdlibIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) with pytest.raises(TypeError) as excinfo: subprocess.Popen(1) @@ -196,31 +336,80 @@ def test_subprocess_invalid_args(sentry_init): assert "'int' object is not iterable" in str(excinfo.value) -def test_subprocess_span_origin(sentry_init, capture_events): - sentry_init(integrations=[StdlibIntegration()], traces_sample_rate=1.0) - events = capture_events() +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_subprocess_span_origin( + sentry_init, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + integrations=[StdlibIntegration()], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + + if span_streaming: + items = capture_items("event", "transaction", "span") - with start_transaction(name="foo"): - args = [ - sys.executable, - "-c", - "print('hello world')", - ] - kw = {"args": args, "stdout": subprocess.PIPE} + with sentry_sdk.traces.start_span(name="custom parent"): + args = [ + sys.executable, + "-c", + "print('hello world')", + ] + kw = {"args": args, "stdout": subprocess.PIPE} - popen = subprocess.Popen(**kw) - popen.communicate() - popen.poll() + popen = subprocess.Popen(**kw) + popen.communicate() + popen.poll() + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + assert spans[3]["attributes"]["sentry.origin"] == "manual" + + assert spans[0]["attributes"]["sentry.op"] == "subprocess" + assert ( + spans[0]["attributes"]["sentry.origin"] + == "auto.subprocess.stdlib.subprocess" + ) + + assert spans[1]["attributes"]["sentry.op"] == "subprocess.wait" + assert ( + spans[1]["attributes"]["sentry.origin"] + == "auto.subprocess.stdlib.subprocess" + ) + + assert spans[2]["attributes"]["sentry.op"] == "subprocess.communicate" + assert ( + spans[2]["attributes"]["sentry.origin"] + == "auto.subprocess.stdlib.subprocess" + ) + else: + events = capture_events() + + with start_transaction(name="foo"): + args = [ + sys.executable, + "-c", + "print('hello world')", + ] + kw = {"args": args, "stdout": subprocess.PIPE} + + popen = subprocess.Popen(**kw) + popen.communicate() + popen.poll() - (event,) = events + (event,) = events - assert event["contexts"]["trace"]["origin"] == "manual" + assert event["contexts"]["trace"]["origin"] == "manual" - assert event["spans"][0]["op"] == "subprocess" - assert event["spans"][0]["origin"] == "auto.subprocess.stdlib.subprocess" + assert event["spans"][0]["op"] == "subprocess" + assert event["spans"][0]["origin"] == "auto.subprocess.stdlib.subprocess" - assert event["spans"][1]["op"] == "subprocess.communicate" - assert event["spans"][1]["origin"] == "auto.subprocess.stdlib.subprocess" + assert event["spans"][1]["op"] == "subprocess.communicate" + assert event["spans"][1]["origin"] == "auto.subprocess.stdlib.subprocess" - assert event["spans"][2]["op"] == "subprocess.wait" - assert event["spans"][2]["origin"] == "auto.subprocess.stdlib.subprocess" + assert event["spans"][2]["op"] == "subprocess.wait" + assert event["spans"][2]["origin"] == "auto.subprocess.stdlib.subprocess"