import json import os from html import unescape from pathlib import Path from django.contrib.auth import get_user_model from django.test import tag from django.utils.html import strip_tags from bugsink.test_utils import TransactionTestCase25251 as TransactionTestCase from events.factories import create_event from issues.utils import get_type_and_value_for_data from projects.models import Project, ProjectMembership SAMPLES_DIR = Path(os.getenv("SAMPLES_DIR", "../event-samples")) def load_sample(relative_path): with open(SAMPLES_DIR / relative_path) as sample: return json.load(sample) def visible_text(response): return " ".join(unescape(strip_tags(response.content.decode())).split()) class StacktraceViewTests(TransactionTestCase): def setUp(self): super().setUp() self.user = get_user_model().objects.create_user(username="test") self.project = Project.objects.create(name="Stacktrace view tests") ProjectMembership.objects.create(project=self.project, user=self.user, accepted=True) self.client.force_login(self.user) def render_stacktrace(self, event_data): # create_event bypasses ingestion; fake the denormalized fields that digest_event would have stored. calculated_type, calculated_value = get_type_and_value_for_data(event_data) event = create_event( project=self.project, event_data=event_data, platform=event_data["platform"], calculated_type=calculated_type, calculated_value=calculated_value, ) return self.client.get(f"/issues/issue/{event.issue.id}/event/{event.id}/") def render_frame_from_sample(self, frame_index): event_data = load_sample("bugsink/frames-with-missing-info.json") frames = event_data["exception"]["values"][0]["stacktrace"]["frames"] event_data["exception"]["values"][0]["stacktrace"]["frames"] = [frames[frame_index]] return self.render_stacktrace(event_data) def test_event_without_stacktrace_says_none_is_available(self): response = self.render_stacktrace({ "platform": "python", }) self.assertContains(response, "No stacktrace available for this event.") def test_exception_displays_type_and_value(self): response = self.render_stacktrace({ "platform": "python", "exception": { "values": [{ "type": "ExampleError", "value": "Something went wrong", }], }, }) self.assertContains(response, "ExampleError") self.assertContains(response, "Something went wrong") def test_frame_displays_location(self): response = self.render_stacktrace({ "platform": "python", "exception": { "values": [{ "type": "ExampleError", "value": "Something went wrong", "stacktrace": { "frames": [{ "filename": "example.py", "function": "do_work", "lineno": 421337, "in_app": True, }], }, }], }, }) self.assertContains(response, "example.py") self.assertContains(response, "do_work") self.assertContains(response, "421337") @tag("samples") def test_frame_displays_source_context_and_variables(self): response = self.render_frame_from_sample(0) text = visible_text(response) self.assertIn("main()", text) self.assertIn("Variable", text) self.assertIn("__name__", text) self.assertNotIn("No code context or variables available for this frame.", text) @tag("samples") def test_frame_without_source_context_displays_variables(self): response = self.render_frame_from_sample(1) text = visible_text(response) self.assertIn("execute_from_command_line", text) self.assertNotIn("No code context or variables available for this frame.", text) @tag("samples") def test_frame_without_variables_displays_source_context(self): response = self.render_frame_from_sample(2) text = visible_text(response) self.assertIn("utility.execute()", text) self.assertNotIn("Variable", text) self.assertNotIn("No code context or variables available for this frame.", text) @tag("samples") def test_frame_without_source_context_or_variables_says_so(self): response = self.render_frame_from_sample(3) self.assertContains(response, "No code context or variables available for this frame.") def test_python_frames_are_displayed_in_payload_order(self): response = self.render_stacktrace({ "platform": "python", "exception": { "values": [{ "type": "ExampleError", "value": "Something went wrong", "stacktrace": { "frames": [ {"filename": "call_started.py"}, {"filename": "error_raised.py"}, ], }, }], }, }) content = response.content.decode() self.assertLess(content.index("call_started.py"), content.index("error_raised.py")) def test_non_python_frames_are_displayed_in_reverse_payload_order(self): response = self.render_stacktrace({ "platform": "java", "exception": { "values": [{ "type": "ExampleError", "value": "Something went wrong", "stacktrace": { "frames": [ {"filename": "CallStarted.java"}, {"filename": "ErrorRaised.java"}, ], }, }], }, }) content = response.content.decode() self.assertLess(content.index("ErrorRaised.java"), content.index("CallStarted.java")) def test_exception_stacktrace_marks_beginning_and_raise(self): response = self.render_stacktrace({ "platform": "python", "exception": { "values": [{ "type": "ExampleError", "value": "Something went wrong", "stacktrace": { "frames": [ {"filename": "call_started.py"}, {"filename": "error_raised.py"}, ], }, }], }, }) content = response.content.decode() positions = [ content.index("call_started.py"), content.index("\u2192 begin"), content.index("error_raised.py"), content.index("raise ExampleError"), ] self.assertEqual(positions, sorted(positions)) def test_handled_exception_stacktrace_marks_try_and_handled_raise(self): response = self.render_stacktrace({ "platform": "python", "exception": { "values": [ { "type": "HandledError", "value": "The first failure", "stacktrace": { "frames": [ {"filename": "handled_call.py"}, {"filename": "handled_raise.py"}, ], }, }, { "type": "FinalError", "value": "The second failure", "stacktrace": { "frames": [ {"filename": "final_call.py"}, {"filename": "final_raise.py"}, ], }, }, ], }, }) content = response.content.decode() positions = [ content.index("handled_call.py"), content.index("try\u2026"), content.index("handled_raise.py"), content.index("raise HandledError (handled)"), content.index("final_call.py"), content.index("\u2192 begin"), content.index("final_raise.py"), content.index("raise FinalError"), ] self.assertEqual(positions, sorted(positions)) @tag("samples") def test_multiple_thread_snapshots_are_displayed_as_sections(self): event_data = load_sample("generated/sentry-java-capture-message-attach-threads.json") text = visible_text(self.render_stacktrace(event_data)) message = "capture_message with all Java threads from ProbeMultipleThreads.java" self.assertEqual(1, text.count(message)) self.assertLess(text.index(message), text.index("Thread #1: main")) self.assertIn("Thread #1: main", text) self.assertIn("Thread #18: Notification Thread", text) self.assertIn("Thread #22: bugsink-probe-background", text) self.assertIn("ProbeMultipleThreads.java in main line 27", text) self.assertIn("ProbeMultipleThreads.java in lambda$main$1 line 19", text) self.assertIn("errored: yes", text) self.assertIn("state: Runnable", text) self.assertNotIn("was active:", text) self.assertEqual(2, text.count("No stacktrace found.")) self.assertNotIn("raise Log Message", text) self.assertNotIn("During handling of the above exception", text) @tag("samples") def test_exception_event_displays_thread_snapshots_after_exception(self): event_data = load_sample("generated/sentry-java-capture-exception-attach-threads.json") text = visible_text(self.render_stacktrace(event_data)) self.assertIn("IllegalStateException", text) self.assertIn("capture_exception with all Java threads from ProbeExceptionMultipleThreads.java", text) self.assertIn("raise IllegalStateException", text) self.assertIn("ProbeExceptionMultipleThreads.java in captureException line 35", text) self.assertLess(text.index("IllegalStateException"), text.index("Threads")) self.assertIn("Thread #1: main", text) self.assertIn("Thread #22: bugsink-probe-background", text) self.assertIn("ProbeExceptionMultipleThreads.java in captureException line 38", text) self.assertIn("ProbeExceptionMultipleThreads.java in lambda$main$1 line 19", text) self.assertEqual(2, text.count("No stacktrace found.")) self.assertNotIn("During handling of the above exception", text) def test_python_exception_chain_is_displayed_in_payload_order(self): response = self.render_stacktrace({ "platform": "python", "exception": { "values": [ { "type": "HandledError", "value": "The first failure", }, { "type": "FinalError", "value": "The second failure", }, ], }, }) content = response.content.decode() separator = "During handling of the above exception another exception occurred or was intentionally reraised:" self.assertLess(content.index("The first failure"), content.index(separator)) self.assertLess(content.index(separator), content.index("The second failure")) def test_non_python_exception_chain_is_displayed_in_reverse_payload_order(self): # Less realistic than a Java sample, but identical inputs make the ordering difference easier to inspect. response = self.render_stacktrace({ "platform": "java", "exception": { "values": [ { "type": "HandledError", "value": "The first failure", }, { "type": "FinalError", "value": "The second failure", }, ], }, }) content = response.content.decode() separator = ( "The above exception was caused by or intentially reraised during the handling of the following exception:" ) self.assertLess(content.index("The second failure"), content.index(separator)) self.assertLess(content.index(separator), content.index("The first failure"))