test_payload_full.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import os.path
  2. import zipfile
  3. from io import BytesIO
  4. from unittest.mock import patch
  5. from uuid import uuid4
  6. import pytest
  7. from django.core.files.uploadedfile import SimpleUploadedFile
  8. from django.urls import reverse
  9. from sentry import eventstore
  10. from sentry.models import (
  11. ArtifactBundle,
  12. DebugIdArtifactBundle,
  13. File,
  14. ProjectDebugFile,
  15. SourceFileType,
  16. )
  17. from sentry.testutils import RelayStoreHelper, TransactionTestCase
  18. from sentry.testutils.factories import get_fixture_path
  19. from sentry.testutils.helpers.datetime import before_now, iso_format
  20. from sentry.testutils.helpers.options import override_options
  21. from sentry.utils import json
  22. from tests.symbolicator import insta_snapshot_native_stacktrace_data, redact_location
  23. # IMPORTANT:
  24. #
  25. # This test suite requires Symbolicator in order to run correctly.
  26. # Set `symbolicator.enabled: true` in your `~/.sentry/config.yml` and run `sentry devservices up`
  27. #
  28. # If you are using a local instance of Symbolicator, you need to
  29. # either change `system.url-prefix` option override inside `initialize` fixture to `system.internal-url-prefix`,
  30. # or add `127.0.0.1 host.docker.internal` entry to your `/etc/hosts`
  31. REAL_RESOLVING_EVENT_DATA = {
  32. "platform": "cocoa",
  33. "debug_meta": {
  34. "images": [
  35. {
  36. "type": "apple",
  37. "arch": "x86_64",
  38. "uuid": "502fc0a5-1ec1-3e47-9998-684fa139dca7",
  39. "image_vmaddr": "0x0000000100000000",
  40. "image_size": 4096,
  41. "image_addr": "0x0000000100000000",
  42. "name": "Foo.app/Contents/Foo",
  43. }
  44. ],
  45. "sdk_info": {
  46. "dsym_type": "macho",
  47. "sdk_name": "macOS",
  48. "version_major": 10,
  49. "version_minor": 12,
  50. "version_patchlevel": 4,
  51. },
  52. },
  53. "exception": {
  54. "values": [
  55. {
  56. "stacktrace": {
  57. "frames": [
  58. {"platform": "foobar", "function": "hi"},
  59. {"function": "unknown", "instruction_addr": "0x0000000100000fa0"},
  60. ]
  61. },
  62. "type": "Fail",
  63. "value": "fail",
  64. }
  65. ]
  66. },
  67. "timestamp": iso_format(before_now(seconds=1)),
  68. }
  69. def get_local_fixture_path(name):
  70. return os.path.join(os.path.dirname(__file__), "fixtures", name)
  71. def load_fixture(name):
  72. with open(get_local_fixture_path(name), "rb") as fp:
  73. return fp.read()
  74. class SymbolicatorResolvingIntegrationTest(RelayStoreHelper, TransactionTestCase):
  75. @pytest.fixture(autouse=True)
  76. def initialize(self, live_server):
  77. self.project.update_option("sentry:builtin_symbol_sources", [])
  78. self.min_ago = iso_format(before_now(minutes=1))
  79. with patch("sentry.auth.system.is_internal_ip", return_value=True), self.options(
  80. {"system.url-prefix": live_server.url}
  81. ):
  82. # Run test case
  83. yield
  84. def get_event(self, event_id):
  85. return eventstore.get_event_by_id(self.project.id, event_id)
  86. def test_real_resolving(self):
  87. url = reverse(
  88. "sentry-api-0-dsym-files",
  89. kwargs={
  90. "organization_slug": self.project.organization.slug,
  91. "project_slug": self.project.slug,
  92. },
  93. )
  94. self.login_as(user=self.user)
  95. out = BytesIO()
  96. f = zipfile.ZipFile(out, "w")
  97. f.write(get_fixture_path("native", "hello.dsym"), "dSYM/hello")
  98. f.close()
  99. response = self.client.post(
  100. url,
  101. {
  102. "file": SimpleUploadedFile(
  103. "symbols.zip", out.getvalue(), content_type="application/zip"
  104. )
  105. },
  106. format="multipart",
  107. )
  108. assert response.status_code == 201, response.content
  109. assert len(response.data) == 1
  110. event = self.post_and_retrieve_event(REAL_RESOLVING_EVENT_DATA)
  111. assert event.data["culprit"] == "main"
  112. candidates = event.data["debug_meta"]["images"][0]["candidates"]
  113. redact_location(candidates)
  114. event.data["debug_meta"]["images"][0]["candidates"] = candidates
  115. insta_snapshot_native_stacktrace_data(self, event.data)
  116. def test_debug_id_resolving(self):
  117. file = File.objects.create(
  118. name="crash.pdb", type="default", headers={"Content-Type": "text/x-breakpad"}
  119. )
  120. path = get_fixture_path("native", "windows.sym")
  121. with open(path, "rb") as f:
  122. file.putfile(f)
  123. ProjectDebugFile.objects.create(
  124. file=file,
  125. object_name="crash.pdb",
  126. cpu_name="x86",
  127. project_id=self.project.id,
  128. debug_id="3249d99d-0c40-4931-8610-f4e4fb0b6936-1",
  129. code_id="5AB380779000",
  130. )
  131. self.login_as(user=self.user)
  132. event_data = {
  133. "contexts": {
  134. "device": {"arch": "x86"},
  135. "os": {"build": "", "name": "Windows", "type": "os", "version": "10.0.14393"},
  136. },
  137. "debug_meta": {
  138. "images": [
  139. {
  140. "id": "3249d99d-0c40-4931-8610-f4e4fb0b6936-1",
  141. "image_addr": "0x2a0000",
  142. "image_size": 36864,
  143. "name": "C:\\projects\\breakpad-tools\\windows\\Release\\crash.exe",
  144. "type": "symbolic",
  145. }
  146. ]
  147. },
  148. "exception": {
  149. "stacktrace": {
  150. "frames": [
  151. {
  152. "function": "<unknown>",
  153. "instruction_addr": "0x2a2a3d",
  154. "package": "C:\\projects\\breakpad-tools\\windows\\Release\\crash.exe",
  155. }
  156. ]
  157. },
  158. "thread_id": 1636,
  159. "type": "EXCEPTION_ACCESS_VIOLATION_WRITE",
  160. "value": "Fatal Error: EXCEPTION_ACCESS_VIOLATION_WRITE",
  161. },
  162. "platform": "native",
  163. "timestamp": iso_format(before_now(seconds=1)),
  164. }
  165. event = self.post_and_retrieve_event(event_data)
  166. assert event.data["culprit"] == "main"
  167. candidates = event.data["debug_meta"]["images"][0]["candidates"]
  168. redact_location(candidates)
  169. event.data["debug_meta"]["images"][0]["candidates"] = candidates
  170. insta_snapshot_native_stacktrace_data(self, event.data)
  171. def test_missing_dsym(self):
  172. self.login_as(user=self.user)
  173. event = self.post_and_retrieve_event(REAL_RESOLVING_EVENT_DATA)
  174. assert event.data["culprit"] == "unknown"
  175. insta_snapshot_native_stacktrace_data(self, event.data)
  176. def test_missing_debug_images(self):
  177. self.login_as(user=self.user)
  178. payload = dict(project=self.project.id, **REAL_RESOLVING_EVENT_DATA)
  179. del payload["debug_meta"]
  180. event = self.post_and_retrieve_event(payload)
  181. assert event.data["culprit"] == "unknown"
  182. insta_snapshot_native_stacktrace_data(self, event.data)
  183. def test_resolving_with_candidates_sentry_source(self):
  184. # Checks the candidates with a sentry source URI for location
  185. file = File.objects.create(
  186. name="crash.pdb", type="default", headers={"Content-Type": "text/x-breakpad"}
  187. )
  188. path = get_fixture_path("native", "windows.sym")
  189. with open(path, "rb") as f:
  190. file.putfile(f)
  191. ProjectDebugFile.objects.create(
  192. file=file,
  193. object_name="crash.pdb",
  194. cpu_name="x86",
  195. project_id=self.project.id,
  196. debug_id="3249d99d-0c40-4931-8610-f4e4fb0b6936-1",
  197. code_id="5AB380779000",
  198. )
  199. self.login_as(user=self.user)
  200. event_data = {
  201. "contexts": {
  202. "device": {"arch": "x86"},
  203. },
  204. "debug_meta": {
  205. "images": [
  206. {
  207. "id": "3249d99d-0c40-4931-8610-f4e4fb0b6936-1",
  208. "image_addr": "0x2a0000",
  209. "image_size": 36864,
  210. "name": "C:\\projects\\breakpad-tools\\windows\\Release\\crash.exe",
  211. "type": "symbolic",
  212. }
  213. ]
  214. },
  215. "exception": {
  216. "stacktrace": {
  217. "frames": [
  218. {
  219. "instruction_addr": "0x2a2a3d",
  220. }
  221. ]
  222. },
  223. "type": "EXCEPTION_ACCESS_VIOLATION_WRITE",
  224. "value": "Fatal Error: EXCEPTION_ACCESS_VIOLATION_WRITE",
  225. },
  226. "platform": "native",
  227. "timestamp": iso_format(before_now(seconds=1)),
  228. }
  229. event = self.post_and_retrieve_event(event_data)
  230. assert event.data["culprit"] == "main"
  231. candidates = event.data["debug_meta"]["images"][0]["candidates"]
  232. redact_location(candidates)
  233. self.insta_snapshot(candidates)
  234. def test_resolve_mixed_stack_trace(self):
  235. # native debug files:
  236. wasm_file = File.objects.create(
  237. name="test.wasm", type="default", headers={"Content-Type": "application/wasm"}
  238. )
  239. with open(get_local_fixture_path("a18fd85d4a4eb893022d6bfad846b1.debug"), "rb") as f:
  240. wasm_file.putfile(f)
  241. # FIXME(swatinem):
  242. # We have a problem, and it seems to be serious! :-(
  243. # The `debug_files` endpoints is using the `ProjectDebugFile.id`, which
  244. # symbolicator is using for caching etc.
  245. # Whereas the `artifact_lookup` is using the `File.id`, which symbolicator
  246. # uses in the same way.
  247. # These two IDs can have collisions !!!
  248. # This can happen right here in this fest when you switch the order
  249. # of creating the native / js files:
  250. # In that case, the `File.id` would be `1`, the same as the `ProjectDebugFile.id`.
  251. # In that case, as we are JS-symbolicating first, symbolicator will fetch
  252. # the bundle with id `1`, and then later on reusing that same cache for
  253. # the wasm file with id `1`, although the cache now already has the JS
  254. # bundle cached as id `1`, and tries to reuse that for wasm, which is wrong.
  255. #
  256. # The symbolicator cache is also keyed on the `scope` (aka project id),
  257. # and it is extremely unlikely to have such collisions in production for
  258. # the same project.
  259. ProjectDebugFile.objects.create(
  260. file=wasm_file,
  261. object_name="test.wasm",
  262. cpu_name="wasm32",
  263. project_id=self.project.id,
  264. debug_id="bda18fd8-5d4a-4eb8-9302-2d6bfad846b1",
  265. code_id="bda18fd85d4a4eb893022d6bfad846b1",
  266. )
  267. # JS debug files:
  268. debug_id = "c941d872-af1f-4f0c-a7ff-ad3d295fe153"
  269. compressed = BytesIO(b"SYSB")
  270. with zipfile.ZipFile(compressed, "a") as zip_file:
  271. zip_file.writestr("files/_/_/test.min.js", load_fixture("test.min.js"))
  272. zip_file.writestr("files/_/_/test.map", load_fixture("test.map"))
  273. zip_file.writestr(
  274. "manifest.json",
  275. json.dumps(
  276. {
  277. "files": {
  278. "files/_/_/test.min.js": {
  279. "url": "~/test.min.js",
  280. "type": "minified_source",
  281. "headers": {
  282. "debug-id": debug_id,
  283. "sourcemap": "test.map",
  284. },
  285. },
  286. "files/_/_/test.map": {
  287. "url": "~/file.wc.sourcemap.js",
  288. "type": "source_map",
  289. "headers": {
  290. "debug-id": debug_id,
  291. },
  292. },
  293. },
  294. }
  295. ),
  296. )
  297. compressed.seek(0)
  298. bundle_file = File.objects.create(name="bundle.zip", type="artifact.bundle")
  299. bundle_file.putfile(compressed)
  300. artifact_bundle = ArtifactBundle.objects.create(
  301. organization_id=self.organization.id,
  302. bundle_id=uuid4(),
  303. file=bundle_file,
  304. artifact_count=2,
  305. )
  306. DebugIdArtifactBundle.objects.create(
  307. organization_id=self.organization.id,
  308. debug_id=debug_id,
  309. artifact_bundle=artifact_bundle,
  310. source_file_type=SourceFileType.MINIFIED_SOURCE.value,
  311. )
  312. DebugIdArtifactBundle.objects.create(
  313. organization_id=self.organization.id,
  314. debug_id=debug_id,
  315. artifact_bundle=artifact_bundle,
  316. source_file_type=SourceFileType.SOURCE_MAP.value,
  317. )
  318. data = {
  319. "timestamp": self.min_ago,
  320. "message": "hello",
  321. "platform": "javascript",
  322. "release": "abc",
  323. "exception": {
  324. "values": [
  325. {
  326. "type": "Error",
  327. "stacktrace": {
  328. "frames": [
  329. {
  330. "abs_path": "http://example.com/test.min.js",
  331. "lineno": 1,
  332. "colno": 183,
  333. },
  334. {
  335. "platform": "native",
  336. "instruction_addr": "0x8c",
  337. "addr_mode": "rel:0",
  338. },
  339. ]
  340. },
  341. }
  342. ]
  343. },
  344. "debug_meta": {
  345. "images": [
  346. {
  347. "type": "sourcemap",
  348. "debug_id": debug_id,
  349. "code_file": "http://example.com/test.min.js",
  350. },
  351. {
  352. "type": "wasm",
  353. "debug_id": "bda18fd8-5d4a-4eb8-9302-2d6bfad846b1",
  354. "code_id": "bda18fd85d4a4eb893022d6bfad846b1",
  355. "debug_file": "file://foo.invalid/demo.wasm",
  356. },
  357. ]
  358. },
  359. }
  360. with override_options({"symbolicator.sourcemaps-processing-sample-rate": 1.0}):
  361. event = self.post_and_retrieve_event(data)
  362. exception = event.interfaces["exception"]
  363. frames = exception.values[0].stacktrace.frames
  364. assert frames[0].abs_path == "http://example.com/test.js"
  365. assert frames[0].lineno == 20
  366. assert frames[0].colno == 5
  367. assert frames[0].context_line == " invoke(data);"
  368. assert frames[1].abs_path == "/Users/mitsuhiko/Development/wasm-example/simple/src/lib.rs"
  369. assert frames[1].lineno == 19
  370. assert frames[1].function == "internal_func"
  371. images = event.data["debug_meta"]["images"]
  372. assert images[1]["debug_status"] == "found"