Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Add integration test for envelope with transaction #671

Merged
merged 6 commits into from
Jan 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)))

Expand Down Expand Up @@ -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 "<Envelope headers=%r items=%r>" % (self.headers, self.items)
Expand All @@ -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 "<Payload bytes=%r json=%r>" % (self.bytes, self.json)
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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 "<Item headers=%r payload=%r>" % (
Expand Down
43 changes: 27 additions & 16 deletions tests/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -27,18 +32,23 @@ 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 = {
"platform": "native",
"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…": "őá…–🤮🚀¿ 한글 테스트"},
}
Expand All @@ -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()},
)
Expand All @@ -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},
)
Expand All @@ -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",
Expand All @@ -82,20 +92,21 @@ 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"}}
)

if integration is 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):
Expand Down Expand Up @@ -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"])


Expand All @@ -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)
Expand Down
59 changes: 59 additions & 0 deletions tests/test_integration_http.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be fine, as the example runs all these on the main thread, however we did run into the issue previously that our time source is not guaranteed to be fully monotonic in multithreaded scenarios :-(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hum, yes i did worry about this. if it turns out to be flaky i'll not hesitate the assert though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh "to remove the assert"