test_process_issue_event.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import uuid
  2. from django.urls import reverse
  3. from apps.issue_events.constants import EventStatus, LogLevel
  4. from apps.issue_events.models import Issue, IssueEvent, IssueHash
  5. from ..process_event import process_issue_events
  6. from ..schema import (
  7. ErrorIssueEventSchema,
  8. InterchangeIssueEvent,
  9. IssueEventSchema,
  10. )
  11. from .utils import EventIngestTestCase
  12. COMPAT_TEST_DATA_DIR = "events/test_data"
  13. def is_exception(v):
  14. return v.get("type") == "exception"
  15. class IssueEventIngestTestCase(EventIngestTestCase):
  16. """
  17. These tests bypass the API and celery. They test the event ingest logic itself.
  18. This file should be large are test the following use cases
  19. - Multiple event saved at the same time
  20. - Sentry API compatibility
  21. - Default, Error, and CSP types
  22. - Graceful failure such as duplicate event ids or invalid data
  23. """
  24. def test_two_events(self):
  25. events = []
  26. for _ in range(2):
  27. payload = IssueEventSchema()
  28. events.append(
  29. InterchangeIssueEvent(project_id=self.project.id, payload=payload)
  30. )
  31. with self.assertNumQueries(12):
  32. process_issue_events(events)
  33. self.assertEqual(Issue.objects.count(), 1)
  34. self.assertEqual(IssueHash.objects.count(), 1)
  35. self.assertEqual(IssueEvent.objects.count(), 2)
  36. def test_reopen_resolved_issue(self):
  37. event = InterchangeIssueEvent(
  38. project_id=self.project.id, payload=IssueEventSchema()
  39. )
  40. process_issue_events([event])
  41. issue = Issue.objects.first()
  42. issue.status = EventStatus.RESOLVED
  43. issue.save()
  44. event.event_id = uuid.uuid4().hex
  45. process_issue_events([event])
  46. issue.refresh_from_db()
  47. self.assertEqual(issue.status, EventStatus.UNRESOLVED)
  48. class SentryCompatTestCase(IssueEventIngestTestCase):
  49. """
  50. These tests specifically test former open source sentry compatibility
  51. But otherwise are part of issue event ingest testing
  52. """
  53. def setUp(self):
  54. super().setUp()
  55. self.create_logged_in_user()
  56. def get_json_test_data(self, name: str):
  57. """Get incoming event, sentry json, sentry api event"""
  58. event = self.get_json_data(
  59. f"{COMPAT_TEST_DATA_DIR}/incoming_events/{name}.json"
  60. )
  61. sentry_json = self.get_json_data(
  62. f"{COMPAT_TEST_DATA_DIR}/oss_sentry_json/{name}.json"
  63. )
  64. # Force captured test data to match test generated data
  65. sentry_json["project"] = self.project.id
  66. api_sentry_event = self.get_json_data(
  67. f"{COMPAT_TEST_DATA_DIR}/oss_sentry_events/{name}.json"
  68. )
  69. return event, sentry_json, api_sentry_event
  70. def get_event_json(self, event: IssueEvent):
  71. return self.client.get(
  72. reverse(
  73. "api:get_event_json",
  74. kwargs={
  75. "organization_slug": self.organization.slug,
  76. "issue_id": event.issue_id,
  77. "event_id": event.id,
  78. },
  79. )
  80. ).json()
  81. # Upgrade functions handle intentional differences between GlitchTip and Sentry OSS
  82. def upgrade_title(self, value: str):
  83. """Sentry OSS uses ... while GlitchTip uses unicode …"""
  84. if value[-1] == "…":
  85. return value[:-4]
  86. return value.strip("...")
  87. def upgrade_metadata(self, value: dict):
  88. value["title"] = self.upgrade_title(value["title"])
  89. return value
  90. def assertCompareData(self, data1: dict, data2: dict, fields: list[str]):
  91. """Compare data of two dict objects. Compare only provided fields list"""
  92. for field in fields:
  93. field_value1 = data1.get(field)
  94. field_value2 = data2.get(field)
  95. if field == "datetime":
  96. # Check that it's close enough
  97. field_value1 = field_value1[:23]
  98. field_value2 = field_value2[:23]
  99. if field == "title" and isinstance(field_value1, str):
  100. field_value1 = self.upgrade_title(field_value1)
  101. if field_value2:
  102. field_value2 = self.upgrade_title(field_value2)
  103. if (
  104. field == "metadata"
  105. and isinstance(field_value1, dict)
  106. and field_value1.get("title")
  107. ):
  108. field_value1 = self.upgrade_metadata(field_value1)
  109. if field_value2:
  110. field_value2 = self.upgrade_metadata(field_value2)
  111. self.assertEqual(
  112. field_value1,
  113. field_value2,
  114. f"Failed for field '{field}'",
  115. )
  116. def get_project_events_detail(self, event_id: str):
  117. return reverse(
  118. "api:get_project_issue_event",
  119. kwargs={
  120. "organization_slug": self.project.organization.slug,
  121. "project_slug": self.project.slug,
  122. "event_id": event_id,
  123. },
  124. )
  125. def submit_event(self, event_data: dict, event_type="error") -> IssueEvent:
  126. event_class = ErrorIssueEventSchema
  127. if event_type == "default":
  128. event_class = IssueEventSchema
  129. event = InterchangeIssueEvent(
  130. event_id=event_data["event_id"],
  131. project_id=self.project.id,
  132. payload=event_class(**event_data),
  133. )
  134. process_issue_events([event])
  135. return IssueEvent.objects.get(pk=event.event_id)
  136. def upgrade_data(self, data):
  137. """A recursive replace function"""
  138. if isinstance(data, dict):
  139. return {k: self.upgrade_data(v) for k, v in data.items()}
  140. elif isinstance(data, list):
  141. return [self.upgrade_data(i) for i in data]
  142. return data
  143. def test_template_error(self):
  144. sdk_error, sentry_json, sentry_data = self.get_json_test_data(
  145. "django_template_error"
  146. )
  147. event = self.submit_event(sdk_error)
  148. url = self.get_project_events_detail(event.id.hex)
  149. res = self.client.get(url)
  150. res_data = res.json()
  151. self.assertEqual(res.status_code, 200)
  152. self.assertCompareData(res_data, sentry_data, ["culprit", "title", "metadata"])
  153. res_frames = res_data["entries"][0]["data"]["values"][0]["stacktrace"]["frames"]
  154. frames = sentry_data["entries"][0]["data"]["values"][0]["stacktrace"]["frames"]
  155. for i in range(6):
  156. # absPath don't always match - needs fixed
  157. self.assertCompareData(res_frames[i], frames[i], ["absPath"])
  158. for res_frame, frame in zip(res_frames, frames):
  159. self.assertCompareData(
  160. res_frame,
  161. frame,
  162. ["lineNo", "function", "filename", "module", "context"],
  163. )
  164. if frame.get("vars"):
  165. self.assertCompareData(
  166. res_frame["vars"], frame["vars"], ["exc", "request"]
  167. )
  168. if frame["vars"].get("get_response"):
  169. # Memory address is different, truncate it
  170. self.assertEqual(
  171. res_frame["vars"]["get_response"][:-16],
  172. frame["vars"]["get_response"][:-16],
  173. )
  174. self.assertCompareData(
  175. res_data["entries"][0]["data"],
  176. sentry_data["entries"][0]["data"],
  177. ["env", "headers", "url", "method", "inferredContentType"],
  178. )
  179. url = reverse("api:get_issue", kwargs={"issue_id": event.issue.pk})
  180. res = self.client.get(url)
  181. self.assertEqual(res.status_code, 200)
  182. res_data = res.json()
  183. data = self.get_json_data("events/test_data/django_template_error_issue.json")
  184. self.assertCompareData(res_data, data, ["title", "metadata"])
  185. def test_js_sdk_with_unix_timestamp(self):
  186. sdk_error, sentry_json, sentry_data = self.get_json_test_data(
  187. "js_event_with_unix_timestamp"
  188. )
  189. event = self.submit_event(sdk_error)
  190. self.assertNotEqual(event.timestamp, sdk_error["timestamp"])
  191. self.assertEqual(event.timestamp.year, 2020)
  192. event_json = self.get_event_json(event)
  193. self.assertCompareData(event_json, sentry_json, ["datetime"])
  194. res = self.client.get(self.get_project_events_detail(event.pk))
  195. res_data = res.json()
  196. self.assertCompareData(res_data, sentry_data, ["timestamp"])
  197. self.assertEqual(res_data["entries"][1].get("type"), "breadcrumbs")
  198. self.maxDiff = None
  199. self.assertEqual(
  200. res_data["entries"][1],
  201. self.upgrade_data(sentry_data["entries"][1]),
  202. )
  203. def test_dotnet_error(self):
  204. sdk_error = self.get_json_data(
  205. "events/test_data/incoming_events/dotnet_error.json"
  206. )
  207. event = self.submit_event(sdk_error)
  208. self.assertEqual(IssueEvent.objects.count(), 1)
  209. sentry_data = self.get_json_data(
  210. "events/test_data/oss_sentry_events/dotnet_error.json"
  211. )
  212. res = self.client.get(self.get_project_events_detail(event.pk))
  213. res_data = res.json()
  214. self.assertCompareData(
  215. res_data,
  216. sentry_data,
  217. ["eventID", "title", "culprit", "platform", "type", "metadata"],
  218. )
  219. res_exception = next(filter(is_exception, res_data["entries"]), None)
  220. sentry_exception = next(filter(is_exception, sentry_data["entries"]), None)
  221. self.assertEqual(
  222. res_exception["data"].get("hasSystemFrames"),
  223. sentry_exception["data"].get("hasSystemFrames"),
  224. )
  225. def test_php_message_event(self):
  226. sdk_error, sentry_json, sentry_data = self.get_json_test_data(
  227. "php_message_event"
  228. )
  229. event = self.submit_event(sdk_error)
  230. res = self.client.get(self.get_project_events_detail(event.pk))
  231. res_data = res.json()
  232. self.assertCompareData(
  233. res_data,
  234. sentry_data,
  235. [
  236. "message",
  237. "title",
  238. ],
  239. )
  240. self.assertEqual(
  241. res_data["entries"][0]["data"]["params"],
  242. sentry_data["entries"][0]["data"]["params"],
  243. )
  244. def test_django_message_params(self):
  245. sdk_error, sentry_json, sentry_data = self.get_json_test_data(
  246. "django_message_params"
  247. )
  248. event = self.submit_event(sdk_error)
  249. res = self.client.get(self.get_project_events_detail(event.pk))
  250. res_data = res.json()
  251. self.assertCompareData(
  252. res_data,
  253. sentry_data,
  254. [
  255. "message",
  256. "title",
  257. ],
  258. )
  259. self.assertEqual(res_data["entries"][0], sentry_data["entries"][0])
  260. def test_message_event(self):
  261. """A generic message made with the Sentry SDK. Generally has less data than exceptions."""
  262. from events.test_data.django_error_factory import message
  263. event = self.submit_event(message, event_type="default")
  264. res = self.client.get(self.get_project_events_detail(event.pk))
  265. res_data = res.json()
  266. data = self.get_json_data("events/test_data/django_message_event.json")
  267. self.assertCompareData(
  268. res_data,
  269. data,
  270. ["title", "culprit", "type", "metadata", "platform", "packages"],
  271. )
  272. def test_python_logging(self):
  273. """Test Sentry SDK logging integration based event"""
  274. sdk_error, sentry_json, sentry_data = self.get_json_test_data("python_logging")
  275. event = self.submit_event(sdk_error, event_type="default")
  276. res = self.client.get(self.get_project_events_detail(event.pk))
  277. res_data = res.json()
  278. self.assertEqual(res.status_code, 200)
  279. self.assertCompareData(
  280. res_data,
  281. sentry_data,
  282. [
  283. "title",
  284. "logentry",
  285. "culprit",
  286. "type",
  287. "metadata",
  288. "platform",
  289. "packages",
  290. ],
  291. )
  292. def test_go_file_not_found(self):
  293. sdk_error = self.get_json_data(
  294. "events/test_data/incoming_events/go_file_not_found.json"
  295. )
  296. event = self.submit_event(sdk_error)
  297. sentry_data = self.get_json_data(
  298. "events/test_data/oss_sentry_events/go_file_not_found.json"
  299. )
  300. res = self.client.get(self.get_project_events_detail(event.pk))
  301. res_data = res.json()
  302. self.assertEqual(res.status_code, 200)
  303. self.assertCompareData(
  304. res_data,
  305. sentry_data,
  306. ["title", "culprit", "type", "metadata", "platform"],
  307. )
  308. def test_very_small_event(self):
  309. """
  310. Shows a very minimalist event example. Good for seeing what data is null
  311. """
  312. sdk_error = self.get_json_data(
  313. "events/test_data/incoming_events/very_small_event.json"
  314. )
  315. event = self.submit_event(sdk_error, event_type="default")
  316. sentry_data = self.get_json_data(
  317. "events/test_data/oss_sentry_events/very_small_event.json"
  318. )
  319. res = self.client.get(self.get_project_events_detail(event.pk))
  320. res_data = res.json()
  321. self.assertEqual(res.status_code, 200)
  322. self.assertCompareData(
  323. res_data,
  324. sentry_data,
  325. ["culprit", "type", "platform", "entries"],
  326. )
  327. def test_python_zero_division(self):
  328. sdk_error, sentry_json, sentry_data = self.get_json_test_data(
  329. "python_zero_division"
  330. )
  331. event = self.submit_event(sdk_error)
  332. event_json = self.get_event_json(event)
  333. self.assertCompareData(
  334. event_json,
  335. sentry_json,
  336. [
  337. "event_id",
  338. "project",
  339. "release",
  340. "dist",
  341. "platform",
  342. "level",
  343. "modules",
  344. "time_spent",
  345. "sdk",
  346. "type",
  347. "title",
  348. "breadcrumbs",
  349. ],
  350. )
  351. self.assertCompareData(
  352. event_json["request"],
  353. sentry_json["request"],
  354. [
  355. "url",
  356. "headers",
  357. "method",
  358. "env",
  359. "query_string",
  360. ],
  361. )
  362. self.assertEqual(
  363. event_json["datetime"][:22],
  364. sentry_json["datetime"][:22],
  365. "Compare if datetime is almost the same",
  366. )
  367. res = self.client.get(self.get_project_events_detail(event.pk))
  368. res_data = res.json()
  369. self.assertEqual(res.status_code, 200)
  370. self.assertCompareData(
  371. res_data,
  372. sentry_data,
  373. ["title", "culprit", "type", "metadata", "platform", "packages"],
  374. )
  375. self.assertCompareData(
  376. res_data["entries"][1]["data"],
  377. sentry_data["entries"][1]["data"],
  378. [
  379. "inferredContentType",
  380. "env",
  381. "headers",
  382. "url",
  383. "query",
  384. "data",
  385. "method",
  386. ],
  387. )
  388. issue = event.issue
  389. issue.refresh_from_db()
  390. self.assertEqual(issue.level, LogLevel.ERROR)
  391. def test_dotnet_zero_division(self):
  392. sdk_error, sentry_json, sentry_data = self.get_json_test_data(
  393. "dotnet_divide_zero"
  394. )
  395. event = self.submit_event(sdk_error)
  396. event_json = self.get_event_json(event)
  397. res = self.client.get(self.get_project_events_detail(event.pk))
  398. res_data = res.json()
  399. self.assertCompareData(event_json, sentry_json, ["environment"])
  400. self.assertCompareData(
  401. res_data,
  402. sentry_data,
  403. [
  404. "eventID",
  405. "title",
  406. "culprit",
  407. "platform",
  408. "type",
  409. "metadata",
  410. ],
  411. )
  412. res_exception = next(filter(is_exception, res_data["entries"]), None)
  413. sentry_exception = next(filter(is_exception, sentry_data["entries"]), None)
  414. self.assertEqual(
  415. res_exception["data"]["values"][0]["stacktrace"]["frames"][4]["context"],
  416. sentry_exception["data"]["values"][0]["stacktrace"]["frames"][4]["context"],
  417. )
  418. # tags = res_data.get("tags")
  419. # browser_tag = next(filter(lambda tag: tag["key"] == "browser", tags), None)
  420. # self.assertEqual(browser_tag["value"], "Firefox 76.0")
  421. # environment_tag = next(
  422. # filter(lambda tag: tag["key"] == "environment", tags), None
  423. # )
  424. # self.assertEqual(environment_tag["value"], "Development")
  425. # event_json = event.event_json()
  426. # browser_tag = next(
  427. # filter(lambda tag: tag[0] == "browser", event_json.get("tags")), None
  428. # )
  429. # self.assertEqual(browser_tag[1], "Firefox 76.0")
  430. # def test_sentry_cli_send_event_no_level(self):
  431. # sdk_error, sentry_json, sentry_data = self.get_json_test_data(
  432. # "sentry_cli_send_event_no_level"
  433. # )
  434. # res = self.client.post(self.event_store_url, sdk_error, format="json")
  435. # event = Event.objects.get(pk=res.data["id"])
  436. # event_json = event.event_json()
  437. # self.assertCompareData(event_json, sentry_json, ["title", "message"])
  438. # self.assertEqual(event_json["project"], event.issue.project_id)
  439. # url = self.get_project_events_detail(event.pk)
  440. # res = self.client.get(url)
  441. # self.assertCompareData(
  442. # res.data,
  443. # sentry_data,
  444. # [
  445. # "userReport",
  446. # "title",
  447. # "culprit",
  448. # "type",
  449. # "metadata",
  450. # "message",
  451. # "platform",
  452. # "previousEventID",
  453. # ],
  454. # )
  455. # self.assertEqual(res.data["projectID"], event.issue.project_id)
  456. # def test_js_error_with_context(self):
  457. # self.project.scrub_ip_addresses = False
  458. # self.project.save()
  459. # sdk_error, sentry_json, sentry_data = self.get_json_test_data(
  460. # "js_error_with_context"
  461. # )
  462. # res = self.client.post(
  463. # self.event_store_url, sdk_error, format="json", REMOTE_ADDR="142.255.29.14"
  464. # )
  465. # event = Event.objects.get(pk=res.data["id"])
  466. # event_json = event.event_json()
  467. # self.assertCompareData(
  468. # event_json, sentry_json, ["title", "message", "extra", "user"]
  469. # )
  470. # url = self.get_project_events_detail(event.pk)
  471. # res = self.client.get(url)
  472. # self.assertCompareData(res.json(), sentry_data, ["context", "user"])
  473. # def test_elixir_stacktrace(self):
  474. # """The elixir SDK does things differently"""
  475. # sdk_error, sentry_json, sentry_data = self.get_json_test_data("elixir_error")
  476. # res = self.client.post(self.event_store_url, sdk_error, format="json")
  477. # event = Event.objects.get(pk=res.data["id"])
  478. # event_json = event.event_json()
  479. # self.assertCompareData(
  480. # event_json["exception"]["values"][0],
  481. # sentry_json["exception"]["values"][0],
  482. # ["type", "values", "exception"],
  483. # )
  484. # def test_small_js_error(self):
  485. # """A small example to test stacktraces"""
  486. # sdk_error, sentry_json, sentry_data = self.get_json_test_data("small_js_error")
  487. # res = self.client.post(self.event_store_url, sdk_error, format="json")
  488. # event = Event.objects.get(pk=res.data["id"])
  489. # event_json = event.event_json()
  490. # self.assertCompareData(
  491. # event_json["exception"]["values"][0],
  492. # sentry_json["exception"]["values"][0],
  493. # ["type", "values", "exception", "abs_path"],
  494. # )