test_sentry_api_compat.py 21 KB

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