test_process_issue_event.py 20 KB

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