test_organization_events_v2.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. from __future__ import absolute_import
  2. import copy
  3. import six
  4. import pytest
  5. import pytz
  6. import time
  7. from sentry.utils.compat.mock import patch
  8. from datetime import timedelta
  9. from six.moves.urllib.parse import urlencode
  10. from sentry.discover.models import DiscoverSavedQuery
  11. from sentry.testutils import AcceptanceTestCase, SnubaTestCase
  12. from sentry.utils.samples import load_data
  13. from sentry.testutils.helpers.datetime import iso_format, before_now
  14. FEATURE_NAMES = [
  15. "organizations:discover-basic",
  16. "organizations:discover-query",
  17. "organizations:transaction-events",
  18. ]
  19. def all_events_query(**kwargs):
  20. options = {
  21. "sort": ["-timestamp"],
  22. "field": ["title", "event.type", "project", "user", "timestamp"],
  23. "name": ["All Events"],
  24. }
  25. options.update(kwargs)
  26. return urlencode(options, doseq=True)
  27. def errors_query(**kwargs):
  28. options = {
  29. "sort": ["-title"],
  30. "name": ["Errors"],
  31. "field": ["title", "count(id)", "count_unique(user)", "project"],
  32. "query": ["event.type:error"],
  33. }
  34. options.update(kwargs)
  35. return urlencode(options, doseq=True)
  36. def transactions_query(**kwargs):
  37. options = {
  38. "sort": ["-count"],
  39. "name": ["Transactions"],
  40. "field": ["transaction", "project", "count()"],
  41. "statsPeriod": ["14d"],
  42. "query": ["event.type:transaction"],
  43. }
  44. options.update(kwargs)
  45. return urlencode(options, doseq=True)
  46. def generate_transaction():
  47. event_data = load_data("transaction")
  48. event_data.update({"event_id": "a" * 32})
  49. # set timestamps
  50. start_datetime = before_now(minutes=1)
  51. end_datetime = start_datetime + timedelta(milliseconds=500)
  52. def generate_timestamp(date_time):
  53. return time.mktime(date_time.utctimetuple()) + date_time.microsecond / 1e6
  54. event_data["start_timestamp"] = generate_timestamp(start_datetime)
  55. event_data["timestamp"] = generate_timestamp(end_datetime)
  56. # generate and build up span tree
  57. reference_span = event_data["spans"][0]
  58. parent_span_id = reference_span["parent_span_id"]
  59. span_tree_blueprint = {
  60. "a": {},
  61. "b": {"bb": {"bbb": {"bbbb": "bbbbb"}}},
  62. "c": {},
  63. "d": {},
  64. "e": {},
  65. }
  66. time_offsets = {
  67. "a": (timedelta(), timedelta(milliseconds=10)),
  68. "b": (timedelta(milliseconds=120), timedelta(milliseconds=250)),
  69. "bb": (timedelta(milliseconds=130), timedelta(milliseconds=10)),
  70. "bbb": (timedelta(milliseconds=140), timedelta(milliseconds=10)),
  71. "bbbb": (timedelta(milliseconds=150), timedelta(milliseconds=10)),
  72. "bbbbb": (timedelta(milliseconds=160), timedelta(milliseconds=90)),
  73. "c": (timedelta(milliseconds=260), timedelta(milliseconds=100)),
  74. "d": (timedelta(milliseconds=375), timedelta(milliseconds=50)),
  75. "e": (timedelta(milliseconds=400), timedelta(milliseconds=100)),
  76. }
  77. def build_span_tree(span_tree, spans, parent_span_id):
  78. for span_id, child in span_tree.items():
  79. span = copy.deepcopy(reference_span)
  80. # non-leaf node span
  81. span["parent_span_id"] = parent_span_id.ljust(16, "0")
  82. span["span_id"] = span_id.ljust(16, "0")
  83. (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta()))
  84. span_start_time = start_datetime + start_delta
  85. span["start_timestamp"] = generate_timestamp(span_start_time)
  86. span["timestamp"] = generate_timestamp(span_start_time + span_length)
  87. spans.append(span)
  88. if isinstance(child, dict):
  89. spans = build_span_tree(child, spans, span_id)
  90. elif isinstance(child, six.string_types):
  91. parent_span_id = span_id
  92. span_id = child
  93. span = copy.deepcopy(reference_span)
  94. # leaf node span
  95. span["parent_span_id"] = parent_span_id.ljust(16, "0")
  96. span["span_id"] = span_id.ljust(16, "0")
  97. (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta()))
  98. span_start_time = start_datetime + start_delta
  99. span["start_timestamp"] = generate_timestamp(span_start_time)
  100. span["timestamp"] = generate_timestamp(span_start_time + span_length)
  101. spans.append(span)
  102. return spans
  103. event_data["spans"] = build_span_tree(span_tree_blueprint, [], parent_span_id)
  104. return event_data
  105. class OrganizationEventsV2Test(AcceptanceTestCase, SnubaTestCase):
  106. def setUp(self):
  107. super(OrganizationEventsV2Test, self).setUp()
  108. self.user = self.create_user("foo@example.com", is_superuser=True)
  109. self.org = self.create_organization(name="Rowdy Tiger")
  110. self.team = self.create_team(organization=self.org, name="Mariachi Band")
  111. self.project = self.create_project(organization=self.org, teams=[self.team], name="Bengal")
  112. self.create_member(user=self.user, organization=self.org, role="owner", teams=[self.team])
  113. self.login_as(self.user)
  114. self.landing_path = u"/organizations/{}/discover/queries/".format(self.org.slug)
  115. self.result_path = u"/organizations/{}/discover/results/".format(self.org.slug)
  116. def wait_until_loaded(self):
  117. self.browser.wait_until_not(".loading-indicator")
  118. self.browser.wait_until_not('[data-test-id="loading-placeholder"]')
  119. def test_events_default_landing(self):
  120. with self.feature(FEATURE_NAMES):
  121. self.browser.get(self.landing_path)
  122. self.wait_until_loaded()
  123. self.browser.snapshot("events-v2 - default landing")
  124. def test_all_events_query_empty_state(self):
  125. with self.feature(FEATURE_NAMES):
  126. self.browser.get(self.result_path + "?" + all_events_query())
  127. self.wait_until_loaded()
  128. self.browser.snapshot("events-v2 - all events query - empty state")
  129. with self.feature(FEATURE_NAMES):
  130. # expect table to expand to the right when no tags are provided
  131. self.browser.get(self.result_path + "?" + all_events_query(tag=[]))
  132. self.wait_until_loaded()
  133. self.browser.snapshot("events-v2 - all events query - empty state - no tags")
  134. @patch("django.utils.timezone.now")
  135. def test_all_events_query(self, mock_now):
  136. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  137. min_ago = iso_format(before_now(minutes=1))
  138. self.store_event(
  139. data={
  140. "event_id": "a" * 32,
  141. "message": "oh no",
  142. "timestamp": min_ago,
  143. "fingerprint": ["group-1"],
  144. },
  145. project_id=self.project.id,
  146. assert_no_errors=False,
  147. )
  148. with self.feature(FEATURE_NAMES):
  149. self.browser.get(self.result_path + "?" + all_events_query())
  150. self.wait_until_loaded()
  151. self.browser.snapshot("events-v2 - all events query - list")
  152. with self.feature(FEATURE_NAMES):
  153. # expect table to expand to the right when no tags are provided
  154. self.browser.get(self.result_path + "?" + all_events_query(tag=[]))
  155. self.wait_until_loaded()
  156. self.browser.snapshot("events-v2 - all events query - list - no tags")
  157. def test_errors_query_empty_state(self):
  158. with self.feature(FEATURE_NAMES):
  159. self.browser.get(self.result_path + "?" + errors_query())
  160. self.wait_until_loaded()
  161. self.browser.snapshot("events-v2 - errors query - empty state")
  162. self.browser.click_when_visible('[data-test-id="grid-edit-enable"]')
  163. self.browser.snapshot(
  164. "events-v2 - errors query - empty state - querybuilder - column edit state"
  165. )
  166. @patch("django.utils.timezone.now")
  167. def test_errors_query(self, mock_now):
  168. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  169. min_ago = iso_format(before_now(minutes=1))
  170. self.store_event(
  171. data={
  172. "event_id": "a" * 32,
  173. "message": "oh no",
  174. "timestamp": min_ago,
  175. "fingerprint": ["group-1"],
  176. "type": "error",
  177. },
  178. project_id=self.project.id,
  179. assert_no_errors=False,
  180. )
  181. self.store_event(
  182. data={
  183. "event_id": "b" * 32,
  184. "message": "oh no",
  185. "timestamp": min_ago,
  186. "fingerprint": ["group-1"],
  187. "type": "error",
  188. },
  189. project_id=self.project.id,
  190. assert_no_errors=False,
  191. )
  192. self.store_event(
  193. data={
  194. "event_id": "c" * 32,
  195. "message": "this is bad.",
  196. "timestamp": min_ago,
  197. "fingerprint": ["group-2"],
  198. "type": "error",
  199. },
  200. project_id=self.project.id,
  201. assert_no_errors=False,
  202. )
  203. with self.feature(FEATURE_NAMES):
  204. self.browser.get(self.result_path + "?" + errors_query())
  205. self.wait_until_loaded()
  206. self.browser.snapshot("events-v2 - errors")
  207. def test_transactions_query_empty_state(self):
  208. with self.feature(FEATURE_NAMES):
  209. self.browser.get(self.result_path + "?" + transactions_query())
  210. self.wait_until_loaded()
  211. self.browser.snapshot("events-v2 - transactions query - empty state")
  212. with self.feature(FEATURE_NAMES):
  213. # expect table to expand to the right when no tags are provided
  214. self.browser.get(self.result_path + "?" + transactions_query(tag=[]))
  215. self.wait_until_loaded()
  216. self.browser.snapshot("events-v2 - transactions query - empty state - no tags")
  217. @patch("django.utils.timezone.now")
  218. def test_transactions_query(self, mock_now):
  219. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  220. event_data = generate_transaction()
  221. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
  222. with self.feature(FEATURE_NAMES):
  223. self.browser.get(self.result_path + "?" + transactions_query())
  224. self.wait_until_loaded()
  225. self.browser.snapshot("events-v2 - transactions query - list")
  226. @patch("django.utils.timezone.now")
  227. def test_event_detail_view_from_all_events(self, mock_now):
  228. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  229. min_ago = iso_format(before_now(minutes=1))
  230. event_data = load_data("python")
  231. event_data.update(
  232. {
  233. "event_id": "a" * 32,
  234. "timestamp": min_ago,
  235. "received": min_ago,
  236. "fingerprint": ["group-1"],
  237. }
  238. )
  239. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=False)
  240. with self.feature(FEATURE_NAMES):
  241. # Get the list page.
  242. self.browser.get(self.result_path + "?" + all_events_query())
  243. self.wait_until_loaded()
  244. # Click the event link to open the events detail view
  245. self.browser.element('[data-test-id="view-events"]').click()
  246. self.wait_until_loaded()
  247. header = self.browser.element('[data-test-id="event-header"] span')
  248. assert event_data["message"] in header.text
  249. self.browser.snapshot("events-v2 - single error details view")
  250. @patch("django.utils.timezone.now")
  251. def test_event_detail_view_from_errors_view(self, mock_now):
  252. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  253. event_source = (("a", 1), ("b", 39), ("c", 69))
  254. event_ids = []
  255. event_data = load_data("javascript")
  256. event_data["fingerprint"] = ["group-1"]
  257. for id_prefix, offset in event_source:
  258. event_time = iso_format(before_now(minutes=offset))
  259. event_data.update(
  260. {
  261. "timestamp": event_time,
  262. "received": event_time,
  263. "event_id": id_prefix * 32,
  264. "type": "error",
  265. }
  266. )
  267. event = self.store_event(data=event_data, project_id=self.project.id)
  268. event_ids.append(event.event_id)
  269. with self.feature(FEATURE_NAMES):
  270. # Get the list page
  271. self.browser.get(self.result_path + "?" + errors_query() + "&statsPeriod=24h")
  272. self.wait_until_loaded()
  273. # Click the event link to open the event detail view
  274. self.browser.element('[data-test-id="view-events"]').click()
  275. self.wait_until_loaded()
  276. self.browser.snapshot("events-v2 - grouped error event detail view")
  277. # Check that the newest event is loaded first and that pagination
  278. # controls display
  279. display_id = self.browser.element('[data-test-id="event-id"]')
  280. assert event_ids[0] in display_id.text
  281. assert self.browser.element_exists_by_test_id("older-event")
  282. assert self.browser.element_exists_by_test_id("newer-event")
  283. @patch("django.utils.timezone.now")
  284. def test_event_detail_view_from_transactions_query(self, mock_now):
  285. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  286. event_data = generate_transaction()
  287. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
  288. with self.feature(FEATURE_NAMES):
  289. # Get the list page
  290. self.browser.get(self.result_path + "?" + transactions_query())
  291. self.wait_until_loaded()
  292. # Click the event link to open the event detail view
  293. self.browser.element('[data-test-id="view-events"]').click()
  294. self.wait_until_loaded()
  295. self.browser.snapshot("events-v2 - transactions event detail view")
  296. def test_create_saved_query(self):
  297. # Simulate a custom query
  298. query = {"field": ["project.id", "count()"], "query": "event.type:error"}
  299. query_name = "A new custom query"
  300. with self.feature(FEATURE_NAMES):
  301. # Go directly to the query builder view
  302. self.browser.get(self.result_path + "?" + urlencode(query, doseq=True))
  303. self.wait_until_loaded()
  304. # Open the save as drawer
  305. self.browser.element('[data-test-id="button-save-as"]').click()
  306. # Fill out name and submit form.
  307. self.browser.element('input[name="query_name"]').send_keys(query_name)
  308. self.browser.element('[data-test-id="button-save-query"]').click()
  309. self.browser.wait_until(
  310. 'div[name="discover2-query-name"][value="{}"]'.format(query_name)
  311. )
  312. # Page title should update.
  313. title_input = self.browser.element('div[name="discover2-query-name"]')
  314. assert title_input.get_attribute("value") == query_name
  315. # Saved query should exist.
  316. assert DiscoverSavedQuery.objects.filter(name=query_name).exists()
  317. def test_view_and_rename_saved_query(self):
  318. # Create saved query to rename
  319. query = DiscoverSavedQuery.objects.create(
  320. name="Custom query",
  321. organization=self.org,
  322. version=2,
  323. query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
  324. )
  325. with self.feature(FEATURE_NAMES):
  326. # View the query list
  327. self.browser.get(self.landing_path)
  328. self.wait_until_loaded()
  329. # Dismiss assistant as it is in the way.
  330. self.browser.element('[aria-label="Got It"]').click()
  331. # Look at the results for our query.
  332. self.browser.element('[data-test-id="card-{}"]'.format(query.name)).click()
  333. self.wait_until_loaded()
  334. input = self.browser.element('div[name="discover2-query-name"]')
  335. input.click()
  336. input.send_keys("updated!")
  337. # Move focus somewhere else to trigger a blur and update the query
  338. self.browser.element("table").click()
  339. new_name = "Custom queryupdated!"
  340. new_card_selector = 'div[name="discover2-query-name"][value="{}"]'.format(new_name)
  341. self.browser.wait_until(new_card_selector)
  342. self.browser.save_screenshot("./rename.png")
  343. # Assert the name was updated.
  344. assert DiscoverSavedQuery.objects.filter(name=new_name).exists()
  345. def test_delete_saved_query(self):
  346. # Create saved query with ORM
  347. query = DiscoverSavedQuery.objects.create(
  348. name="Custom query",
  349. organization=self.org,
  350. version=2,
  351. query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
  352. )
  353. with self.feature(FEATURE_NAMES):
  354. # View the query list
  355. self.browser.get(self.landing_path)
  356. self.wait_until_loaded()
  357. # Get the card with the new query
  358. card_selector = '[data-test-id="card-{}"]'.format(query.name)
  359. card = self.browser.element(card_selector)
  360. # Open the context menu
  361. card.find_element_by_css_selector('[data-test-id="context-menu"]').click()
  362. # Delete the query
  363. card.find_element_by_css_selector('[data-test-id="delete-query"]').click()
  364. # Wait for card to clear
  365. self.browser.wait_until_not(card_selector)
  366. assert DiscoverSavedQuery.objects.filter(name=query.name).exists() is False
  367. def test_duplicate_query(self):
  368. # Create saved query with ORM
  369. query = DiscoverSavedQuery.objects.create(
  370. name="Custom query",
  371. organization=self.org,
  372. version=2,
  373. query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
  374. )
  375. with self.feature(FEATURE_NAMES):
  376. # View the query list
  377. self.browser.get(self.landing_path)
  378. self.wait_until_loaded()
  379. # Get the card with the new query
  380. card_selector = '[data-test-id="card-{}"]'.format(query.name)
  381. card = self.browser.element(card_selector)
  382. # Open the context menu, and duplicate
  383. card.find_element_by_css_selector('[data-test-id="context-menu"]').click()
  384. card.find_element_by_css_selector('[data-test-id="duplicate-query"]').click()
  385. duplicate_name = "{} copy".format(query.name)
  386. # Wait for new element to show up.
  387. self.browser.element('[data-test-id="card-{}"]'.format(duplicate_name))
  388. # Assert the new query exists and has 'copy' added to the name.
  389. assert DiscoverSavedQuery.objects.filter(name=duplicate_name).exists()
  390. @pytest.mark.skip(reason="not done")
  391. @patch("django.utils.timezone.now")
  392. def test_usage(self, mock_now):
  393. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  394. # TODO: load events
  395. # go to landing
  396. # go to a precanned query
  397. # save query 1
  398. # add environment column
  399. # update query
  400. # add condition from facet map
  401. # delete a column
  402. # click and drag a column
  403. # save as query 2
  404. # load save query 1
  405. # sort column
  406. # update query
  407. # delete save query 1