diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 6884190fd6..7fe4609cf3 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -8,7 +8,9 @@ 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.tracing import TransactionSource +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, @@ -183,50 +185,111 @@ def on_operation(self) -> "Generator[None, None, None]": event_processor = _make_request_event_processor(self.execution_context) scope.add_event_processor(event_processor) - graphql_span = sentry_sdk.start_span( - op=op, - name=description, - origin=StrawberryIntegration.origin, - ) - graphql_span.__enter__() + 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) - if should_send_default_pii(): - graphql_span.set_data("graphql.document", self.execution_context.query) - graphql_span.set_data("graphql.resource_name", self._resource_name) + 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 - 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 + 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]": - validation_span = sentry_sdk.start_span( - op=OP.GRAPHQL_VALIDATE, - name="validation", - origin=StrawberryIntegration.origin, - ) + 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, + ) yield - validation_span.finish() + if isinstance(validation_span, StreamedSpan): + validation_span.end() + else: + validation_span.finish() def on_parse(self) -> "Generator[None, None, None]": - parsing_span = sentry_sdk.start_span( - op=OP.GRAPHQL_PARSE, - name="parsing", - origin=StrawberryIntegration.origin, - ) + 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, + ) yield - parsing_span.finish() + if isinstance(parsing_span, StreamedSpan): + parsing_span.end() + else: + parsing_span.finish() def should_skip_tracing( self, @@ -263,6 +326,18 @@ async def resolve( 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), @@ -290,6 +365,18 @@ def resolve( 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), diff --git a/tests/integrations/strawberry/test_strawberry.py b/tests/integrations/strawberry/test_strawberry.py index 3a509a835d..16e15142ff 100644 --- a/tests/integrations/strawberry/test_strawberry.py +++ b/tests/integrations/strawberry/test_strawberry.py @@ -2,6 +2,8 @@ import pytest +import sentry_sdk + strawberry = pytest.importorskip("strawberry") pytest.importorskip("fastapi") pytest.importorskip("flask") @@ -287,14 +289,17 @@ def test_breadcrumb_no_operation_name( "send_default_pii", [True, False], ) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_capture_transaction_on_error( request, sentry_init, capture_events, + capture_items, client_factory, async_execution, framework_integrations, send_default_pii, + span_streaming, ): sentry_init( send_default_pii=send_default_pii, @@ -303,8 +308,13 @@ def test_capture_transaction_on_error( ] + framework_integrations, traces_sample_rate=1, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + + if span_streaming: + items = capture_items("event", "span") + else: + events = capture_events() schema = strawberry.Schema(Query) @@ -314,59 +324,109 @@ def test_capture_transaction_on_error( query = "query ErrorQuery { error }" client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) - assert len(events) == 2 - (_, transaction_event) = events - - assert transaction_event["transaction"] == "ErrorQuery" - assert transaction_event["contexts"]["trace"]["op"] == OP.GRAPHQL_QUERY - assert transaction_event["spans"] - - query_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY - ] - assert len(query_spans) == 1, "exactly one query span expected" - query_span = query_spans[0] - assert query_span["description"] == "query ErrorQuery" - assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] == "ErrorQuery" - assert query_span["data"]["graphql.resource_name"] - - if send_default_pii is True: - assert query_span["data"]["graphql.document"] == query + if span_streaming: + sentry_sdk.flush() + error_events = [i.payload for i in items if i.type == "event"] + spans = [i.payload for i in items if i.type == "span"] + + assert len(error_events) == 1 + + if async_execution: + # When FastAPI is run, there's an extra span from the httpx client + # so we need to account for that + assert len(spans) == 6 + parse_span, validate_span, resolve_span, query_span, segment, _ = spans + else: + assert len(spans) == 5 + parse_span, validate_span, resolve_span, query_span, segment = spans + + assert segment["is_segment"] is True + assert segment["name"] == "ErrorQuery" + assert segment["attributes"]["sentry.op"] == OP.GRAPHQL_QUERY + + assert query_span["attributes"]["sentry.op"] == OP.GRAPHQL_QUERY + assert query_span["name"] == "query ErrorQuery" + assert query_span["attributes"]["graphql.operation.type"] == "query" + assert query_span["attributes"]["graphql.operation.name"] == "ErrorQuery" + + if send_default_pii is True: + assert query_span["attributes"]["graphql.document"] == query + else: + assert "graphql.document" not in query_span["attributes"] + + assert parse_span["attributes"]["sentry.op"] == OP.GRAPHQL_PARSE + assert parse_span["name"] == "parsing" + assert parse_span["parent_span_id"] == query_span["span_id"] + + assert validate_span["attributes"]["sentry.op"] == OP.GRAPHQL_VALIDATE + assert validate_span["name"] == "validation" + assert validate_span["parent_span_id"] == query_span["span_id"] + + assert resolve_span["attributes"]["sentry.op"] == OP.GRAPHQL_RESOLVE + assert resolve_span["name"] == "resolving Query.error" + assert resolve_span["parent_span_id"] == query_span["span_id"] else: - assert "graphql.document" not in query_span["data"] - - parse_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE - ] - assert len(parse_spans) == 1, "exactly one parse span expected" - parse_span = parse_spans[0] - assert parse_span["parent_span_id"] == query_span["span_id"] - assert parse_span["description"] == "parsing" - - validate_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE - ] - assert len(validate_spans) == 1, "exactly one validate span expected" - validate_span = validate_spans[0] - assert validate_span["parent_span_id"] == query_span["span_id"] - assert validate_span["description"] == "validation" - - resolve_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE - ] - assert len(resolve_spans) == 1, "exactly one resolve span expected" - resolve_span = resolve_spans[0] - assert resolve_span["parent_span_id"] == query_span["span_id"] - assert resolve_span["description"] == "resolving Query.error" - assert resolve_span["data"] == ApproxDict( - { - "graphql.field_name": "error", - "graphql.parent_type": "Query", - "graphql.field_path": "Query.error", - "graphql.path": "error", - } - ) + assert len(events) == 2 + (_, transaction_event) = events + + assert transaction_event["transaction"] == "ErrorQuery" + assert transaction_event["contexts"]["trace"]["op"] == OP.GRAPHQL_QUERY + assert transaction_event["spans"] + + query_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_QUERY + ] + assert len(query_spans) == 1, "exactly one query span expected" + query_span = query_spans[0] + assert query_span["description"] == "query ErrorQuery" + assert query_span["data"]["graphql.operation.type"] == "query" + assert query_span["data"]["graphql.operation.name"] == "ErrorQuery" + assert query_span["data"]["graphql.resource_name"] + + if send_default_pii is True: + assert query_span["data"]["graphql.document"] == query + else: + assert "graphql.document" not in query_span["data"] + + parse_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Query.error" + assert resolve_span["data"] == ApproxDict( + { + "graphql.field_name": "error", + "graphql.parent_type": "Query", + "graphql.field_path": "Query.error", + "graphql.path": "error", + } + ) @parameterize_strawberry_test @@ -374,14 +434,17 @@ def test_capture_transaction_on_error( "send_default_pii", [True, False], ) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_capture_transaction_on_success( request, sentry_init, capture_events, + capture_items, client_factory, async_execution, framework_integrations, send_default_pii, + span_streaming, ): sentry_init( integrations=[ @@ -390,8 +453,13 @@ def test_capture_transaction_on_success( + framework_integrations, traces_sample_rate=1, send_default_pii=send_default_pii, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() schema = strawberry.Schema(Query) @@ -401,59 +469,104 @@ def test_capture_transaction_on_success( query = "query GreetingQuery { hello }" client.post("/graphql", json={"query": query, "operationName": "GreetingQuery"}) - assert len(events) == 1 - (transaction_event,) = events - - assert transaction_event["transaction"] == "GreetingQuery" - assert transaction_event["contexts"]["trace"]["op"] == OP.GRAPHQL_QUERY - assert transaction_event["spans"] - - query_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY - ] - assert len(query_spans) == 1, "exactly one query span expected" - query_span = query_spans[0] - assert query_span["description"] == "query GreetingQuery" - assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] == "GreetingQuery" - assert query_span["data"]["graphql.resource_name"] - - if send_default_pii is True: - assert query_span["data"]["graphql.document"] == query + if span_streaming: + sentry_sdk.flush() + spans = [i.payload for i in items] + + if async_execution: + assert len(spans) == 6 + parse_span, validate_span, resolve_span, query_span, segment, _ = spans + else: + assert len(spans) == 5 + parse_span, validate_span, resolve_span, query_span, segment = spans + + assert segment["is_segment"] is True + assert segment["name"] == "GreetingQuery" + assert segment["attributes"]["sentry.op"] == OP.GRAPHQL_QUERY + + assert query_span["attributes"]["sentry.op"] == OP.GRAPHQL_QUERY + assert query_span["name"] == "query GreetingQuery" + assert query_span["attributes"]["graphql.operation.type"] == "query" + assert query_span["attributes"]["graphql.operation.name"] == "GreetingQuery" + + if send_default_pii is True: + assert query_span["attributes"]["graphql.document"] == query + else: + assert "graphql.document" not in query_span["attributes"] + + assert parse_span["attributes"]["sentry.op"] == OP.GRAPHQL_PARSE + assert parse_span["name"] == "parsing" + assert parse_span["parent_span_id"] == query_span["span_id"] + + assert validate_span["attributes"]["sentry.op"] == OP.GRAPHQL_VALIDATE + assert validate_span["name"] == "validation" + assert validate_span["parent_span_id"] == query_span["span_id"] + + assert resolve_span["attributes"]["sentry.op"] == OP.GRAPHQL_RESOLVE + assert resolve_span["name"] == "resolving Query.hello" + assert resolve_span["parent_span_id"] == query_span["span_id"] else: - assert "graphql.document" not in query_span["data"] - - parse_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE - ] - assert len(parse_spans) == 1, "exactly one parse span expected" - parse_span = parse_spans[0] - assert parse_span["parent_span_id"] == query_span["span_id"] - assert parse_span["description"] == "parsing" - - validate_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE - ] - assert len(validate_spans) == 1, "exactly one validate span expected" - validate_span = validate_spans[0] - assert validate_span["parent_span_id"] == query_span["span_id"] - assert validate_span["description"] == "validation" - - resolve_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE - ] - assert len(resolve_spans) == 1, "exactly one resolve span expected" - resolve_span = resolve_spans[0] - assert resolve_span["parent_span_id"] == query_span["span_id"] - assert resolve_span["description"] == "resolving Query.hello" - assert resolve_span["data"] == ApproxDict( - { - "graphql.field_name": "hello", - "graphql.parent_type": "Query", - "graphql.field_path": "Query.hello", - "graphql.path": "hello", - } - ) + assert len(events) == 1 + (transaction_event,) = events + + assert transaction_event["transaction"] == "GreetingQuery" + assert transaction_event["contexts"]["trace"]["op"] == OP.GRAPHQL_QUERY + assert transaction_event["spans"] + + query_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_QUERY + ] + assert len(query_spans) == 1, "exactly one query span expected" + query_span = query_spans[0] + assert query_span["description"] == "query GreetingQuery" + assert query_span["data"]["graphql.operation.type"] == "query" + assert query_span["data"]["graphql.operation.name"] == "GreetingQuery" + assert query_span["data"]["graphql.resource_name"] + + if send_default_pii is True: + assert query_span["data"]["graphql.document"] == query + else: + assert "graphql.document" not in query_span["data"] + + parse_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Query.hello" + assert resolve_span["data"] == ApproxDict( + { + "graphql.field_name": "hello", + "graphql.parent_type": "Query", + "graphql.field_path": "Query.hello", + "graphql.path": "hello", + } + ) @parameterize_strawberry_test @@ -461,14 +574,17 @@ def test_capture_transaction_on_success( "send_default_pii", [True, False], ) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_transaction_no_operation_name( request, sentry_init, capture_events, + capture_items, client_factory, async_execution, framework_integrations, send_default_pii, + span_streaming, ): sentry_init( integrations=[ @@ -477,8 +593,13 @@ def test_transaction_no_operation_name( + framework_integrations, traces_sample_rate=1, send_default_pii=send_default_pii, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() schema = strawberry.Schema(Query) @@ -488,62 +609,109 @@ def test_transaction_no_operation_name( query = "{ hello }" client.post("/graphql", json={"query": query}) - assert len(events) == 1 - (transaction_event,) = events - - if async_execution: - assert transaction_event["transaction"] == "/graphql" + if span_streaming: + sentry_sdk.flush() + spans = [i.payload for i in items] + + if async_execution: + assert len(spans) == 6 + parse_span, validate_span, resolve_span, query_span, segment, _ = spans + else: + assert len(spans) == 5 + parse_span, validate_span, resolve_span, query_span, segment = spans + + assert segment["is_segment"] is True + if async_execution: + assert segment["name"] == "/graphql" + else: + assert segment["name"] == "graphql_view" + + assert query_span["attributes"]["sentry.op"] == OP.GRAPHQL_QUERY + assert query_span["name"] == "query" + assert query_span["attributes"]["graphql.operation.type"] == "query" + assert "graphql.operation.name" not in query_span["attributes"] + + if send_default_pii is True: + assert query_span["attributes"]["graphql.document"] == query + else: + assert "graphql.document" not in query_span["attributes"] + + assert parse_span["attributes"]["sentry.op"] == OP.GRAPHQL_PARSE + assert parse_span["name"] == "parsing" + assert parse_span["parent_span_id"] == query_span["span_id"] + + assert validate_span["attributes"]["sentry.op"] == OP.GRAPHQL_VALIDATE + assert validate_span["name"] == "validation" + assert validate_span["parent_span_id"] == query_span["span_id"] + + assert resolve_span["attributes"]["sentry.op"] == OP.GRAPHQL_RESOLVE + assert resolve_span["name"] == "resolving Query.hello" + assert resolve_span["parent_span_id"] == query_span["span_id"] else: - assert transaction_event["transaction"] == "graphql_view" - - assert transaction_event["spans"] - - query_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY - ] - assert len(query_spans) == 1, "exactly one query span expected" - query_span = query_spans[0] - assert query_span["description"] == "query" - assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] is None - assert query_span["data"]["graphql.resource_name"] - - if send_default_pii is True: - assert query_span["data"]["graphql.document"] == query - else: - assert "graphql.document" not in query_span["data"] - - parse_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE - ] - assert len(parse_spans) == 1, "exactly one parse span expected" - parse_span = parse_spans[0] - assert parse_span["parent_span_id"] == query_span["span_id"] - assert parse_span["description"] == "parsing" - - validate_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE - ] - assert len(validate_spans) == 1, "exactly one validate span expected" - validate_span = validate_spans[0] - assert validate_span["parent_span_id"] == query_span["span_id"] - assert validate_span["description"] == "validation" - - resolve_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE - ] - assert len(resolve_spans) == 1, "exactly one resolve span expected" - resolve_span = resolve_spans[0] - assert resolve_span["parent_span_id"] == query_span["span_id"] - assert resolve_span["description"] == "resolving Query.hello" - assert resolve_span["data"] == ApproxDict( - { - "graphql.field_name": "hello", - "graphql.parent_type": "Query", - "graphql.field_path": "Query.hello", - "graphql.path": "hello", - } - ) + assert len(events) == 1 + (transaction_event,) = events + + if async_execution: + assert transaction_event["transaction"] == "/graphql" + else: + assert transaction_event["transaction"] == "graphql_view" + + assert transaction_event["spans"] + + query_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_QUERY + ] + assert len(query_spans) == 1, "exactly one query span expected" + query_span = query_spans[0] + assert query_span["description"] == "query" + assert query_span["data"]["graphql.operation.type"] == "query" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.resource_name"] + + if send_default_pii is True: + assert query_span["data"]["graphql.document"] == query + else: + assert "graphql.document" not in query_span["data"] + + parse_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Query.hello" + assert resolve_span["data"] == ApproxDict( + { + "graphql.field_name": "hello", + "graphql.parent_type": "Query", + "graphql.field_path": "Query.hello", + "graphql.path": "hello", + } + ) @parameterize_strawberry_test @@ -551,14 +719,17 @@ def test_transaction_no_operation_name( "send_default_pii", [True, False], ) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_transaction_mutation( request, sentry_init, capture_events, + capture_items, client_factory, async_execution, framework_integrations, send_default_pii, + span_streaming, ): sentry_init( integrations=[ @@ -567,8 +738,13 @@ def test_transaction_mutation( + framework_integrations, traces_sample_rate=1, send_default_pii=send_default_pii, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() schema = strawberry.Schema(Query, mutation=Mutation) @@ -578,59 +754,104 @@ def test_transaction_mutation( query = 'mutation Change { change(attribute: "something") }' client.post("/graphql", json={"query": query}) - assert len(events) == 1 - (transaction_event,) = events - - assert transaction_event["transaction"] == "Change" - assert transaction_event["contexts"]["trace"]["op"] == OP.GRAPHQL_MUTATION - assert transaction_event["spans"] - - query_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_MUTATION - ] - assert len(query_spans) == 1, "exactly one mutation span expected" - query_span = query_spans[0] - assert query_span["description"] == "mutation" - assert query_span["data"]["graphql.operation.type"] == "mutation" - assert query_span["data"]["graphql.operation.name"] is None - assert query_span["data"]["graphql.resource_name"] - - if send_default_pii is True: - assert query_span["data"]["graphql.document"] == query + if span_streaming: + sentry_sdk.flush() + spans = [i.payload for i in items] + + if async_execution: + assert len(spans) == 6 + parse_span, validate_span, resolve_span, mutation_span, segment, _ = spans + else: + assert len(spans) == 5 + parse_span, validate_span, resolve_span, mutation_span, segment = spans + + assert segment["is_segment"] is True + assert segment["name"] == "Change" + assert segment["attributes"]["sentry.op"] == OP.GRAPHQL_MUTATION + + assert mutation_span["attributes"]["sentry.op"] == OP.GRAPHQL_MUTATION + assert mutation_span["name"] == "mutation" + assert mutation_span["attributes"]["graphql.operation.type"] == "mutation" + assert "graphql.operation.name" not in mutation_span["attributes"] + + if send_default_pii is True: + assert mutation_span["attributes"]["graphql.document"] == query + else: + assert "graphql.document" not in mutation_span["attributes"] + + assert parse_span["attributes"]["sentry.op"] == OP.GRAPHQL_PARSE + assert parse_span["name"] == "parsing" + assert parse_span["parent_span_id"] == mutation_span["span_id"] + + assert validate_span["attributes"]["sentry.op"] == OP.GRAPHQL_VALIDATE + assert validate_span["name"] == "validation" + assert validate_span["parent_span_id"] == mutation_span["span_id"] + + assert resolve_span["attributes"]["sentry.op"] == OP.GRAPHQL_RESOLVE + assert resolve_span["name"] == "resolving Mutation.change" + assert resolve_span["parent_span_id"] == mutation_span["span_id"] else: - assert "graphql.document" not in query_span["data"] - - parse_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE - ] - assert len(parse_spans) == 1, "exactly one parse span expected" - parse_span = parse_spans[0] - assert parse_span["parent_span_id"] == query_span["span_id"] - assert parse_span["description"] == "parsing" - - validate_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE - ] - assert len(validate_spans) == 1, "exactly one validate span expected" - validate_span = validate_spans[0] - assert validate_span["parent_span_id"] == query_span["span_id"] - assert validate_span["description"] == "validation" - - resolve_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE - ] - assert len(resolve_spans) == 1, "exactly one resolve span expected" - resolve_span = resolve_spans[0] - assert resolve_span["parent_span_id"] == query_span["span_id"] - assert resolve_span["description"] == "resolving Mutation.change" - assert resolve_span["data"] == ApproxDict( - { - "graphql.field_name": "change", - "graphql.parent_type": "Mutation", - "graphql.field_path": "Mutation.change", - "graphql.path": "change", - } - ) + assert len(events) == 1 + (transaction_event,) = events + + assert transaction_event["transaction"] == "Change" + assert transaction_event["contexts"]["trace"]["op"] == OP.GRAPHQL_MUTATION + assert transaction_event["spans"] + + query_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_MUTATION + ] + assert len(query_spans) == 1, "exactly one mutation span expected" + query_span = query_spans[0] + assert query_span["description"] == "mutation" + assert query_span["data"]["graphql.operation.type"] == "mutation" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.resource_name"] + + if send_default_pii is True: + assert query_span["data"]["graphql.document"] == query + else: + assert "graphql.document" not in query_span["data"] + + parse_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Mutation.change" + assert resolve_span["data"] == ApproxDict( + { + "graphql.field_name": "change", + "graphql.parent_type": "Mutation", + "graphql.field_path": "Mutation.change", + "graphql.path": "change", + } + ) @parameterize_strawberry_test @@ -661,13 +882,16 @@ def test_handle_none_query_gracefully( @parameterize_strawberry_test +@pytest.mark.parametrize("span_streaming", [True, False]) def test_span_origin( request, sentry_init, capture_events, + capture_items, client_factory, async_execution, framework_integrations, + span_streaming, ): """ Tests for OP.GRAPHQL_MUTATION, OP.GRAPHQL_PARSE, OP.GRAPHQL_VALIDATE, OP.GRAPHQL_RESOLVE, @@ -678,8 +902,13 @@ def test_span_origin( ] + framework_integrations, traces_sample_rate=1, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() schema = strawberry.Schema(Query, mutation=Mutation) @@ -689,27 +918,51 @@ def test_span_origin( query = 'mutation Change { change(attribute: "something") }' client.post("/graphql", json={"query": query}) - (event,) = events - is_flask = "Flask" in str(framework_integrations[0]) - if is_flask: - assert event["contexts"]["trace"]["origin"] == "auto.http.flask" + + if span_streaming: + sentry_sdk.flush() + spans = [i.payload for i in items] + + if async_execution: + assert len(spans) == 6 + parse_span, validate_span, resolve_span, mutation_span, segment, _ = spans + else: + assert len(spans) == 5 + parse_span, validate_span, resolve_span, mutation_span, segment = spans + + assert segment["is_segment"] is True + if is_flask: + assert segment["attributes"]["sentry.origin"] == "auto.http.flask" + else: + assert segment["attributes"]["sentry.origin"] == "auto.http.starlette" + + for span in (parse_span, validate_span, resolve_span, mutation_span): + assert span["attributes"]["sentry.origin"] == "auto.graphql.strawberry" else: - assert event["contexts"]["trace"]["origin"] == "auto.http.starlette" + (event,) = events - for span in event["spans"]: - if span["op"].startswith("graphql."): - assert span["origin"] == "auto.graphql.strawberry" + if is_flask: + assert event["contexts"]["trace"]["origin"] == "auto.http.flask" + else: + assert event["contexts"]["trace"]["origin"] == "auto.http.starlette" + + for span in event["spans"]: + if span["op"].startswith("graphql."): + assert span["origin"] == "auto.graphql.strawberry" @parameterize_strawberry_test +@pytest.mark.parametrize("span_streaming", [True, False]) def test_span_origin2( request, sentry_init, capture_events, + capture_items, client_factory, async_execution, framework_integrations, + span_streaming, ): """ Tests for OP.GRAPHQL_QUERY @@ -720,8 +973,13 @@ def test_span_origin2( ] + framework_integrations, traces_sample_rate=1, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() schema = strawberry.Schema(Query, mutation=Mutation) @@ -731,27 +989,51 @@ def test_span_origin2( query = "query GreetingQuery { hello }" client.post("/graphql", json={"query": query, "operationName": "GreetingQuery"}) - (event,) = events - is_flask = "Flask" in str(framework_integrations[0]) - if is_flask: - assert event["contexts"]["trace"]["origin"] == "auto.http.flask" + + if span_streaming: + sentry_sdk.flush() + spans = [i.payload for i in items] + + if async_execution: + assert len(spans) == 6 + parse_span, validate_span, resolve_span, query_span, segment, _ = spans + else: + assert len(spans) == 5 + parse_span, validate_span, resolve_span, query_span, segment = spans + + assert segment["is_segment"] is True + if is_flask: + assert segment["attributes"]["sentry.origin"] == "auto.http.flask" + else: + assert segment["attributes"]["sentry.origin"] == "auto.http.starlette" + + for span in (parse_span, validate_span, resolve_span, query_span): + assert span["attributes"]["sentry.origin"] == "auto.graphql.strawberry" else: - assert event["contexts"]["trace"]["origin"] == "auto.http.starlette" + (event,) = events + + if is_flask: + assert event["contexts"]["trace"]["origin"] == "auto.http.flask" + else: + assert event["contexts"]["trace"]["origin"] == "auto.http.starlette" - for span in event["spans"]: - if span["op"].startswith("graphql."): - assert span["origin"] == "auto.graphql.strawberry" + for span in event["spans"]: + if span["op"].startswith("graphql."): + assert span["origin"] == "auto.graphql.strawberry" @parameterize_strawberry_test +@pytest.mark.parametrize("span_streaming", [True, False]) def test_span_origin3( request, sentry_init, capture_events, + capture_items, client_factory, async_execution, framework_integrations, + span_streaming, ): """ Tests for OP.GRAPHQL_SUBSCRIPTION @@ -762,8 +1044,13 @@ def test_span_origin3( ] + framework_integrations, traces_sample_rate=1, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() schema = strawberry.Schema(Query, subscription=Subscription) @@ -773,14 +1060,37 @@ def test_span_origin3( query = "subscription { messageAdded { content } }" client.post("/graphql", json={"query": query}) - (event,) = events - is_flask = "Flask" in str(framework_integrations[0]) - if is_flask: - assert event["contexts"]["trace"]["origin"] == "auto.http.flask" + + if span_streaming: + sentry_sdk.flush() + spans = [i.payload for i in items] + + if async_execution: + assert len(spans) == 6 + parse_span, validate_span, resolve_span, subscription_span, segment, _ = ( + spans + ) + else: + assert len(spans) == 5 + parse_span, validate_span, resolve_span, subscription_span, segment = spans + + assert segment["is_segment"] is True + if is_flask: + assert segment["attributes"]["sentry.origin"] == "auto.http.flask" + else: + assert segment["attributes"]["sentry.origin"] == "auto.http.starlette" + + for span in (parse_span, validate_span, resolve_span, subscription_span): + assert span["attributes"]["sentry.origin"] == "auto.graphql.strawberry" else: - assert event["contexts"]["trace"]["origin"] == "auto.http.starlette" + (event,) = events + + if is_flask: + assert event["contexts"]["trace"]["origin"] == "auto.http.flask" + else: + assert event["contexts"]["trace"]["origin"] == "auto.http.starlette" - for span in event["spans"]: - if span["op"].startswith("graphql."): - assert span["origin"] == "auto.graphql.strawberry" + for span in event["spans"]: + if span["op"].startswith("graphql."): + assert span["origin"] == "auto.graphql.strawberry"