test_organization_events_v2.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. from __future__ import absolute_import
  2. import copy
  3. import six
  4. import pytest
  5. import pytz
  6. from sentry.utils.compat.mock import patch
  7. from datetime import timedelta
  8. from six.moves.urllib.parse import urlencode
  9. from selenium.webdriver.common.keys import Keys
  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, timestamp_format
  14. FEATURE_NAMES = [
  15. "organizations:discover-basic",
  16. "organizations:discover-query",
  17. "organizations:performance-view",
  18. ]
  19. def all_events_query(**kwargs):
  20. options = {
  21. "sort": ["-timestamp"],
  22. "field": ["title", "event.type", "project", "user.display", "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(trace=None, span=None):
  47. start_datetime = before_now(minutes=1, milliseconds=500)
  48. end_datetime = before_now(minutes=1)
  49. event_data = load_data(
  50. "transaction",
  51. timestamp=end_datetime,
  52. start_timestamp=start_datetime,
  53. trace=trace,
  54. span=span,
  55. )
  56. event_data.update({"event_id": "a" * 32})
  57. # generate and build up span tree
  58. reference_span = event_data["spans"][0]
  59. parent_span_id = reference_span["parent_span_id"]
  60. span_tree_blueprint = {
  61. "a": {},
  62. "b": {"bb": {"bbb": {"bbbb": "bbbbb"}}},
  63. "c": {},
  64. "d": {},
  65. "e": {},
  66. }
  67. time_offsets = {
  68. "a": (timedelta(), timedelta(milliseconds=10)),
  69. "b": (timedelta(milliseconds=120), timedelta(milliseconds=250)),
  70. "bb": (timedelta(milliseconds=130), timedelta(milliseconds=10)),
  71. "bbb": (timedelta(milliseconds=140), timedelta(milliseconds=10)),
  72. "bbbb": (timedelta(milliseconds=150), timedelta(milliseconds=10)),
  73. "bbbbb": (timedelta(milliseconds=160), timedelta(milliseconds=90)),
  74. "c": (timedelta(milliseconds=260), timedelta(milliseconds=100)),
  75. "d": (timedelta(milliseconds=375), timedelta(milliseconds=50)),
  76. "e": (timedelta(milliseconds=400), timedelta(milliseconds=100)),
  77. }
  78. def build_span_tree(span_tree, spans, parent_span_id):
  79. for span_id, child in sorted(span_tree.items(), key=lambda item: item[0]):
  80. span = copy.deepcopy(reference_span)
  81. # non-leaf node span
  82. span["parent_span_id"] = parent_span_id.ljust(16, "0")
  83. span["span_id"] = span_id.ljust(16, "0")
  84. (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta()))
  85. span_start_time = start_datetime + start_delta
  86. span["start_timestamp"] = timestamp_format(span_start_time)
  87. span["timestamp"] = timestamp_format(span_start_time + span_length)
  88. spans.append(span)
  89. if isinstance(child, dict):
  90. spans = build_span_tree(child, spans, span_id)
  91. elif isinstance(child, six.string_types):
  92. parent_span_id = span_id
  93. span_id = child
  94. span = copy.deepcopy(reference_span)
  95. # leaf node span
  96. span["parent_span_id"] = parent_span_id.ljust(16, "0")
  97. span["span_id"] = span_id.ljust(16, "0")
  98. (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta()))
  99. span_start_time = start_datetime + start_delta
  100. span["start_timestamp"] = timestamp_format(span_start_time)
  101. span["timestamp"] = timestamp_format(span_start_time + span_length)
  102. spans.append(span)
  103. return spans
  104. event_data["spans"] = build_span_tree(span_tree_blueprint, [], parent_span_id)
  105. return event_data
  106. class OrganizationEventsV2Test(AcceptanceTestCase, SnubaTestCase):
  107. def setUp(self):
  108. super(OrganizationEventsV2Test, self).setUp()
  109. self.user = self.create_user("foo@example.com", is_superuser=True)
  110. self.org = self.create_organization(name="Rowdy Tiger")
  111. self.team = self.create_team(organization=self.org, name="Mariachi Band")
  112. self.project = self.create_project(organization=self.org, teams=[self.team], name="Bengal")
  113. self.create_member(user=self.user, organization=self.org, role="owner", teams=[self.team])
  114. self.login_as(self.user)
  115. self.landing_path = u"/organizations/{}/discover/queries/".format(self.org.slug)
  116. self.result_path = u"/organizations/{}/discover/results/".format(self.org.slug)
  117. def wait_until_loaded(self):
  118. self.browser.wait_until_not(".loading-indicator")
  119. self.browser.wait_until_not('[data-test-id="loading-placeholder"]')
  120. def test_events_default_landing(self):
  121. with self.feature(FEATURE_NAMES):
  122. self.browser.get(self.landing_path)
  123. self.wait_until_loaded()
  124. self.browser.snapshot("events-v2 - default landing")
  125. def test_all_events_query_empty_state(self):
  126. with self.feature(FEATURE_NAMES):
  127. self.browser.get(self.result_path + "?" + all_events_query())
  128. self.wait_until_loaded()
  129. self.browser.snapshot("events-v2 - all events query - empty state")
  130. with self.feature(FEATURE_NAMES):
  131. # expect table to expand to the right when no tags are provided
  132. self.browser.get(self.result_path + "?" + all_events_query(tag=[]))
  133. self.wait_until_loaded()
  134. self.browser.snapshot("events-v2 - all events query - empty state - no tags")
  135. @patch("django.utils.timezone.now")
  136. def test_all_events_query(self, mock_now):
  137. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  138. min_ago = iso_format(before_now(minutes=1))
  139. two_min_ago = iso_format(before_now(minutes=2))
  140. self.store_event(
  141. data={
  142. "event_id": "a" * 32,
  143. "message": "oh no",
  144. "timestamp": min_ago,
  145. "fingerprint": ["group-1"],
  146. },
  147. project_id=self.project.id,
  148. assert_no_errors=False,
  149. )
  150. self.store_event(
  151. data={
  152. "event_id": "b" * 32,
  153. "message": "this is bad.",
  154. "timestamp": two_min_ago,
  155. "fingerprint": ["group-2"],
  156. "user": {
  157. "id": "123",
  158. "email": "someone@example.com",
  159. "username": "haveibeenpwned",
  160. "ip_address": "8.8.8.8",
  161. "name": "Someone",
  162. },
  163. },
  164. project_id=self.project.id,
  165. assert_no_errors=False,
  166. )
  167. with self.feature(FEATURE_NAMES):
  168. self.browser.get(self.result_path + "?" + all_events_query())
  169. self.wait_until_loaded()
  170. self.browser.snapshot("events-v2 - all events query - list")
  171. with self.feature(FEATURE_NAMES):
  172. # expect table to expand to the right when no tags are provided
  173. self.browser.get(self.result_path + "?" + all_events_query(tag=[]))
  174. self.wait_until_loaded()
  175. self.browser.snapshot("events-v2 - all events query - list - no tags")
  176. def test_errors_query_empty_state(self):
  177. with self.feature(FEATURE_NAMES):
  178. self.browser.get(self.result_path + "?" + errors_query())
  179. self.wait_until_loaded()
  180. self.browser.snapshot("events-v2 - errors query - empty state")
  181. self.browser.click_when_visible('[data-test-id="grid-edit-enable"]')
  182. self.browser.snapshot(
  183. "events-v2 - errors query - empty state - querybuilder - column edit state"
  184. )
  185. @patch("django.utils.timezone.now")
  186. def test_errors_query(self, mock_now):
  187. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  188. min_ago = iso_format(before_now(minutes=1))
  189. self.store_event(
  190. data={
  191. "event_id": "a" * 32,
  192. "message": "oh no",
  193. "timestamp": min_ago,
  194. "fingerprint": ["group-1"],
  195. "type": "error",
  196. },
  197. project_id=self.project.id,
  198. assert_no_errors=False,
  199. )
  200. self.store_event(
  201. data={
  202. "event_id": "b" * 32,
  203. "message": "oh no",
  204. "timestamp": min_ago,
  205. "fingerprint": ["group-1"],
  206. "type": "error",
  207. },
  208. project_id=self.project.id,
  209. assert_no_errors=False,
  210. )
  211. self.store_event(
  212. data={
  213. "event_id": "c" * 32,
  214. "message": "this is bad.",
  215. "timestamp": min_ago,
  216. "fingerprint": ["group-2"],
  217. "type": "error",
  218. },
  219. project_id=self.project.id,
  220. assert_no_errors=False,
  221. )
  222. with self.feature(FEATURE_NAMES):
  223. self.browser.get(self.result_path + "?" + errors_query())
  224. self.wait_until_loaded()
  225. self.browser.snapshot("events-v2 - errors")
  226. def test_transactions_query_empty_state(self):
  227. with self.feature(FEATURE_NAMES):
  228. self.browser.get(self.result_path + "?" + transactions_query())
  229. self.wait_until_loaded()
  230. self.browser.snapshot("events-v2 - transactions query - empty state")
  231. with self.feature(FEATURE_NAMES):
  232. # expect table to expand to the right when no tags are provided
  233. self.browser.get(self.result_path + "?" + transactions_query(tag=[]))
  234. self.wait_until_loaded()
  235. self.browser.snapshot("events-v2 - transactions query - empty state - no tags")
  236. @patch("django.utils.timezone.now")
  237. def test_transactions_query(self, mock_now):
  238. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  239. event_data = generate_transaction()
  240. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
  241. with self.feature(FEATURE_NAMES):
  242. self.browser.get(self.result_path + "?" + transactions_query())
  243. self.wait_until_loaded()
  244. self.browser.snapshot("events-v2 - transactions query - list")
  245. @patch("django.utils.timezone.now")
  246. def test_event_detail_view_from_all_events(self, mock_now):
  247. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  248. min_ago = iso_format(before_now(minutes=1))
  249. event_data = load_data("python")
  250. event_data.update(
  251. {
  252. "event_id": "a" * 32,
  253. "timestamp": min_ago,
  254. "received": min_ago,
  255. "fingerprint": ["group-1"],
  256. }
  257. )
  258. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=False)
  259. with self.feature(FEATURE_NAMES):
  260. # Get the list page.
  261. self.browser.get(self.result_path + "?" + all_events_query())
  262. self.wait_until_loaded()
  263. # View Event
  264. self.browser.elements('[data-test-id="view-event"]')[0].click()
  265. self.wait_until_loaded()
  266. header = self.browser.element('[data-test-id="event-header"] span')
  267. assert event_data["message"] in header.text
  268. self.browser.snapshot("events-v2 - single error details view")
  269. @patch("django.utils.timezone.now")
  270. def test_event_detail_view_from_errors_view(self, mock_now):
  271. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  272. event_data = load_data("javascript")
  273. event_data.update(
  274. {
  275. "timestamp": iso_format(before_now(minutes=5)),
  276. "event_id": "d" * 32,
  277. "fingerprint": ["group-1"],
  278. }
  279. )
  280. self.store_event(data=event_data, project_id=self.project.id)
  281. with self.feature(FEATURE_NAMES):
  282. # Get the list page
  283. self.browser.get(self.result_path + "?" + errors_query() + "&statsPeriod=24h")
  284. self.wait_until_loaded()
  285. # Open the stack
  286. self.browser.element('[data-test-id="open-stack"]').click()
  287. self.wait_until_loaded()
  288. # View Event
  289. self.browser.elements('[data-test-id="view-event"]')[0].click()
  290. self.wait_until_loaded()
  291. self.browser.snapshot("events-v2 - error event detail view")
  292. @patch("django.utils.timezone.now")
  293. def test_event_detail_view_from_transactions_query(self, mock_now):
  294. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  295. event_data = generate_transaction(trace="a" * 32, span="ab" * 8)
  296. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
  297. # Create a child event that is linked to the parent so we have coverage
  298. # of traversal buttons.
  299. child_event = generate_transaction(
  300. trace=event_data["contexts"]["trace"]["trace_id"], span="bc" * 8
  301. )
  302. child_event["event_id"] = "b" * 32
  303. child_event["contexts"]["trace"]["parent_span_id"] = event_data["spans"][4]["span_id"]
  304. child_event["transaction"] = "z-child-transaction"
  305. child_event["spans"] = child_event["spans"][0:3]
  306. self.store_event(data=child_event, project_id=self.project.id, assert_no_errors=True)
  307. with self.feature(FEATURE_NAMES):
  308. # Get the list page
  309. self.browser.get(self.result_path + "?" + transactions_query())
  310. self.wait_until_loaded()
  311. # Open the stack
  312. self.browser.elements('[data-test-id="open-stack"]')[0].click()
  313. self.wait_until_loaded()
  314. # View Event
  315. self.browser.elements('[data-test-id="view-event"]')[0].click()
  316. self.wait_until_loaded()
  317. # Open a span detail so we can check the search by trace link.
  318. # Click on the 6th one as a missing instrumentation span is inserted.
  319. self.browser.elements('[data-test-id="span-row"]')[6].click()
  320. # Wait until the child event loads.
  321. child_button = '[data-test-id="view-child-transaction"]'
  322. self.browser.wait_until(child_button)
  323. self.browser.snapshot("events-v2 - transactions event detail view")
  324. # Click on the child transaction.
  325. self.browser.click(child_button)
  326. self.wait_until_loaded()
  327. @patch("django.utils.timezone.now")
  328. def test_transaction_event_detail_view_ops_filtering(self, mock_now):
  329. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  330. event_data = generate_transaction(trace="a" * 32, span="ab" * 8)
  331. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
  332. with self.feature(FEATURE_NAMES):
  333. # Get the list page
  334. self.browser.get(self.result_path + "?" + transactions_query())
  335. self.wait_until_loaded()
  336. # Open the stack
  337. self.browser.elements('[data-test-id="open-stack"]')[0].click()
  338. self.wait_until_loaded()
  339. # View Event
  340. self.browser.elements('[data-test-id="view-event"]')[0].click()
  341. self.wait_until_loaded()
  342. # Interact with ops filter dropdown
  343. self.browser.elements('[data-test-id="filter-button"]')[0].click()
  344. # select all ops
  345. self.browser.elements(
  346. '[data-test-id="op-filter-dropdown"] [data-test-id="checkbox-fancy"]'
  347. )[0].click()
  348. # un-select django.middleware
  349. self.browser.elements(
  350. '[data-test-id="op-filter-dropdown"] [data-test-id="checkbox-fancy"]'
  351. )[1].click()
  352. self.browser.snapshot("events-v2 - transactions event detail view - ops filtering")
  353. def test_create_saved_query(self):
  354. # Simulate a custom query
  355. query = {"field": ["project.id", "count()"], "query": "event.type:error"}
  356. query_name = "A new custom query"
  357. with self.feature(FEATURE_NAMES):
  358. # Go directly to the query builder view
  359. self.browser.get(self.result_path + "?" + urlencode(query, doseq=True))
  360. self.wait_until_loaded()
  361. # Open the save as drawer
  362. self.browser.element('[data-test-id="button-save-as"]').click()
  363. # Fill out name and submit form.
  364. self.browser.element('input[name="query_name"]').send_keys(query_name)
  365. self.browser.element('[data-test-id="button-save-query"]').click()
  366. self.browser.wait_until(
  367. 'div[name="discover2-query-name"][value="{}"]'.format(query_name)
  368. )
  369. # Page title should update.
  370. title_input = self.browser.element('div[name="discover2-query-name"]')
  371. assert title_input.get_attribute("value") == query_name
  372. # Saved query should exist.
  373. assert DiscoverSavedQuery.objects.filter(name=query_name).exists()
  374. def test_view_and_rename_saved_query(self):
  375. # Create saved query to rename
  376. query = DiscoverSavedQuery.objects.create(
  377. name="Custom query",
  378. organization=self.org,
  379. version=2,
  380. query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
  381. )
  382. with self.feature(FEATURE_NAMES):
  383. # View the query list
  384. self.browser.get(self.landing_path)
  385. self.wait_until_loaded()
  386. # Look at the results for our query.
  387. self.browser.element('[data-test-id="card-{}"]'.format(query.name)).click()
  388. self.wait_until_loaded()
  389. input = self.browser.element('div[name="discover2-query-name"]')
  390. input.click()
  391. input.send_keys(Keys.END + "updated!")
  392. # Move focus somewhere else to trigger a blur and update the query
  393. self.browser.element("table").click()
  394. new_name = "Custom queryupdated!"
  395. new_card_selector = 'div[name="discover2-query-name"][value="{}"]'.format(new_name)
  396. self.browser.wait_until(new_card_selector)
  397. # Assert the name was updated.
  398. assert DiscoverSavedQuery.objects.filter(name=new_name).exists()
  399. def test_delete_saved_query(self):
  400. # Create saved query with ORM
  401. query = DiscoverSavedQuery.objects.create(
  402. name="Custom query",
  403. organization=self.org,
  404. version=2,
  405. query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
  406. )
  407. with self.feature(FEATURE_NAMES):
  408. # View the query list
  409. self.browser.get(self.landing_path)
  410. self.wait_until_loaded()
  411. # Get the card with the new query
  412. card_selector = '[data-test-id="card-{}"]'.format(query.name)
  413. card = self.browser.element(card_selector)
  414. # Open the context menu
  415. card.find_element_by_css_selector('[data-test-id="context-menu"]').click()
  416. # Delete the query
  417. card.find_element_by_css_selector('[data-test-id="delete-query"]').click()
  418. # Wait for card to clear
  419. self.browser.wait_until_not(card_selector)
  420. assert DiscoverSavedQuery.objects.filter(name=query.name).exists() is False
  421. def test_duplicate_query(self):
  422. # Create saved query with ORM
  423. query = DiscoverSavedQuery.objects.create(
  424. name="Custom query",
  425. organization=self.org,
  426. version=2,
  427. query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
  428. )
  429. with self.feature(FEATURE_NAMES):
  430. # View the query list
  431. self.browser.get(self.landing_path)
  432. self.wait_until_loaded()
  433. # Get the card with the new query
  434. card_selector = '[data-test-id="card-{}"]'.format(query.name)
  435. card = self.browser.element(card_selector)
  436. # Open the context menu, and duplicate
  437. card.find_element_by_css_selector('[data-test-id="context-menu"]').click()
  438. card.find_element_by_css_selector('[data-test-id="duplicate-query"]').click()
  439. duplicate_name = "{} copy".format(query.name)
  440. # Wait for new element to show up.
  441. self.browser.element('[data-test-id="card-{}"]'.format(duplicate_name))
  442. # Assert the new query exists and has 'copy' added to the name.
  443. assert DiscoverSavedQuery.objects.filter(name=duplicate_name).exists()
  444. @pytest.mark.skip(reason="causing timeouts in github actions and travis")
  445. @patch("django.utils.timezone.now")
  446. def test_drilldown_result(self, mock_now):
  447. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  448. min_ago = iso_format(before_now(minutes=1))
  449. events = (
  450. ("a" * 32, "oh no", "group-1"),
  451. ("b" * 32, "oh no", "group-1"),
  452. ("c" * 32, "this is bad", "group-2"),
  453. )
  454. for event in events:
  455. self.store_event(
  456. data={
  457. "event_id": event[0],
  458. "message": event[1],
  459. "timestamp": min_ago,
  460. "fingerprint": [event[2]],
  461. "type": "error",
  462. },
  463. project_id=self.project.id,
  464. )
  465. query = {"field": ["message", "project", "count()"], "query": "event.type:error"}
  466. with self.feature(FEATURE_NAMES):
  467. # Go directly to the query builder view
  468. self.browser.get(self.result_path + "?" + urlencode(query, doseq=True))
  469. self.wait_until_loaded()
  470. # Click the first drilldown
  471. self.browser.element('[data-test-id="expand-count"]').click()
  472. self.wait_until_loaded()
  473. assert self.browser.element_exists_by_test_id("grid-editable"), "table should exist."
  474. headers = self.browser.elements('[data-test-id="grid-editable"] thead th')
  475. expected = ["", "MESSAGE", "PROJECT", "ID"]
  476. actual = [header.text for header in headers]
  477. assert expected == actual
  478. @pytest.mark.skip(reason="not done")
  479. @patch("django.utils.timezone.now")
  480. def test_usage(self, mock_now):
  481. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  482. # TODO: load events
  483. # go to landing
  484. # go to a precanned query
  485. # save query 1
  486. # add environment column
  487. # update query
  488. # add condition from facet map
  489. # delete a column
  490. # click and drag a column
  491. # save as query 2
  492. # load save query 1
  493. # sort column
  494. # update query
  495. # delete save query 1