From c1919370a942c0bf2a7332aa4964428e5f4c0bda Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 27 Jan 2022 09:33:39 +0100 Subject: [PATCH] test: Add integration test for envelope with transaction (#671) This adds an integration test which inspects the envelope of a transaction sent by the native SDK. NATIVE-407 --- tests/__init__.py | 50 ++++++++++++++++++++++++++-- tests/assertions.py | 43 ++++++++++++++++--------- tests/test_integration_http.py | 59 ++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 18 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 02f614664..b34e1c2ea 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,6 +5,8 @@ import sys import urllib import pytest +import pprint +import textwrap sourcedir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) @@ -165,6 +167,19 @@ def deserialize( # type: (...) -> Envelope return cls.deserialize_from(io.BytesIO(bytes)) + def print_verbose(self, indent=0): + """Pretty prints the envelope.""" + print(" " * indent + "Envelope:") + indent += 2 + print(" " * indent + "Headers:") + indent += 2 + print(textwrap.indent(pprint.pformat(self.headers), " " * indent)) + indent -= 2 + print(" " * indent + "Items:") + indent += 2 + for item in self.items: + item.print_verbose(indent) + def __repr__(self): # type: (...) -> str return "" % (self.headers, self.items) @@ -180,6 +195,21 @@ def __init__( self.json = json self.bytes = bytes + def print_verbose(self, indent=0): + """Pretty prints the item.""" + print(" " * indent + "Payload:") + indent += 2 + if self.bytes: + payload = self.bytes.encode("utf-8", errors="replace") + if self.json: + payload = pprint.pformat(self.json) + try: + print(textwrap.indent(payload, " " * indent)) + except UnicodeEncodeError: + # Windows CI appears fickle, and we put emojis in the json + payload = payload.encode("ascii", errors="replace").decode() + print(textwrap.indent(payload, " " * indent)) + def __repr__(self): # type: (...) -> str return "" % (self.bytes, self.json) @@ -205,7 +235,11 @@ def __init__( def get_event(self): # type: (...) -> Optional[Event] - if self.headers.get("type") == "event" and self.payload.json is not None: + # Transactions are events with a few extra fields, so return them as well. + if ( + self.headers.get("type") in ["event", "transaction"] + and self.payload.json is not None + ): return self.payload.json return None @@ -220,7 +254,7 @@ def deserialize_from( headers = json.loads(line) length = headers["length"] payload = f.read(length) - if headers.get("type") == "event" or headers.get("type") == "session": + if headers.get("type") in ["event", "session", "transaction"]: rv = cls(headers=headers, payload=PayloadRef(json=json.loads(payload))) else: rv = cls(headers=headers, payload=payload) @@ -234,6 +268,18 @@ def deserialize( # type: (...) -> Optional[Item] return cls.deserialize_from(io.BytesIO(bytes)) + def print_verbose(self, indent=0): + """Pretty prints the item.""" + item_type = self.headers.get("type", "?").capitalize() + print(" " * indent + f"{item_type}:") + indent += 2 + print(" " * indent + "Headers:") + indent += 2 + headers = pprint.pformat(self.headers) + print(textwrap.indent(headers, " " * indent)) + indent -= 2 + self.payload.print_verbose(indent) + def __repr__(self): # type: (...) -> str return "" % ( diff --git a/tests/assertions.py b/tests/assertions.py index 379686f2e..7f4f06112 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -14,6 +14,11 @@ def matches(actual, expected): return {k: v for (k, v) in actual.items() if k in expected.keys()} == expected +def assert_matches(actual, expected): + """Assert two objects for equality, ignoring extra keys in ``actual``.""" + assert {k: v for (k, v) in actual.items() if k in expected.keys()} == expected + + def assert_session(envelope, extra_assertion=None): session = None for item in envelope: @@ -27,10 +32,15 @@ def assert_session(envelope, extra_assertion=None): "environment": "development", } if extra_assertion: - assert matches(session, extra_assertion) + assert_matches(session, extra_assertion) -def assert_meta(envelope, release="test-example-release", integration=None): +def assert_meta( + envelope, + release="test-example-release", + integration=None, + transaction="test-transaction", +): event = envelope.get_event() expected = { @@ -38,7 +48,7 @@ def assert_meta(envelope, release="test-example-release", integration=None): "environment": "development", "release": release, "user": {"id": 42, "username": "some_name"}, - "transaction": "test-transaction", + "transaction": transaction, "tags": {"expected-tag": "some value"}, "extra": {"extra stuff": "some value", "…unicode key…": "őá…–🤮🚀¿ 한글 테스트"}, } @@ -51,7 +61,7 @@ def assert_meta(envelope, release="test-example-release", integration=None): } if not is_android: if sys.platform == "win32": - assert matches( + assert_matches( event["contexts"]["os"], {"name": "Windows", "version": platform.version()}, ) @@ -62,7 +72,7 @@ def assert_meta(envelope, release="test-example-release", integration=None): version = match.group(1) build = match.group(2) - assert matches( + assert_matches( event["contexts"]["os"], {"name": "Linux", "version": version, "build": build}, ) @@ -72,7 +82,7 @@ def assert_meta(envelope, release="test-example-release", integration=None): version.append("0") version = ".".join(version) - assert matches( + assert_matches( event["contexts"]["os"], { "name": "macOS", @@ -82,9 +92,9 @@ def assert_meta(envelope, release="test-example-release", integration=None): ) assert event["contexts"]["os"]["build"] is not None - assert matches(event, expected) - assert matches(event["sdk"], expected_sdk) - assert matches( + assert_matches(event, expected) + assert_matches(event["sdk"], expected_sdk) + assert_matches( event["contexts"], {"runtime": {"type": "runtime", "name": "testing-runtime"}} ) @@ -92,10 +102,11 @@ def assert_meta(envelope, release="test-example-release", integration=None): assert event["sdk"].get("integrations") is None else: assert event["sdk"]["integrations"] == [integration] - assert any( - "sentry_example" in image["code_file"] - for image in event["debug_meta"]["images"] - ) + if event.get("type") == "event": + assert any( + "sentry_example" in image["code_file"] + for image in event["debug_meta"]["images"] + ) def assert_stacktrace(envelope, inside_exception=False, check_size=True): @@ -155,7 +166,7 @@ def assert_event(envelope): "logger": "my-logger", "message": {"formatted": "Hello World!"}, } - assert matches(event, expected) + assert_matches(event, expected) assert_timestamp(event["timestamp"]) @@ -165,13 +176,13 @@ def assert_exception(envelope): "type": "ParseIntError", "value": "invalid digit found in string", } - assert matches(event["exception"]["values"][0], exception) + assert_matches(event["exception"]["values"][0], exception) assert_timestamp(event["timestamp"]) def assert_crash(envelope): event = envelope.get_event() - assert matches(event, {"level": "fatal"}) + assert_matches(event, {"level": "fatal"}) # depending on the unwinder, we currently don’t get any stack frames from # a `ucontext` assert_stacktrace(envelope, inside_exception=True, check_size=False) diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 1e5f6f46d..6148bbf07 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -1,9 +1,11 @@ +import time import pytest import subprocess import sys import os import time import itertools +import uuid import json from . import make_dsn, check_output, run, Envelope from .conditions import has_http, has_breakpad, has_files @@ -413,3 +415,60 @@ def delayed(req): run(tmp_path, "sentry_example", ["log", "no-setup"], check=True, env=env) assert len(httpserver.log) == 10 + + +def test_transaction_only(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver), SENTRY_RELEASE="🤮🚀") + + run( + tmp_path, + "sentry_example", + ["log", "capture-transaction"], + check=True, + env=env, + ) + + assert len(httpserver.log) == 1 + output = httpserver.log[0][0].get_data() + envelope = Envelope.deserialize(output) + + # Show what the envelope looks like if the test fails. + envelope.print_verbose() + + # The transaction is overwritten. + assert_meta(envelope, transaction="little.teapot") + + # Extract the one-and-only-item + (event,) = envelope.items + + assert event.headers["type"] == "transaction" + json = event.payload.json + + # See https://develop.sentry.dev/sdk/performance/trace-context/#trace-context + trace_context = json["contexts"]["trace"] + + assert ( + trace_context["op"] == "Short and stout here is my handle and here is my spout" + ) + + assert trace_context["trace_id"] + trace_id = uuid.UUID(hex=trace_context["trace_id"]) + assert trace_id + + # TODO: currently missing + # assert trace_context['public_key'] + + assert trace_context["span_id"] + assert trace_context["status"] == "ok" + + RFC3339_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" + start_timestamp = time.strptime(json["start_timestamp"], RFC3339_FORMAT) + assert start_timestamp + timestamp = time.strptime(json["timestamp"], RFC3339_FORMAT) + assert timestamp >= start_timestamp