test_organization_events_v2.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. import copy
  2. from datetime import timedelta
  3. from unittest.mock import patch
  4. from urllib.parse import urlencode
  5. import pytest
  6. import pytz
  7. from selenium.webdriver.common.by import By
  8. from selenium.webdriver.common.keys import Keys
  9. from sentry.discover.models import DiscoverSavedQuery
  10. from sentry.testutils import AcceptanceTestCase, SnubaTestCase
  11. from sentry.testutils.helpers.datetime import before_now, iso_format, timestamp_format
  12. from sentry.utils.samples import load_data
  13. FEATURE_NAMES = [
  14. "organizations:discover-basic",
  15. "organizations:discover-query",
  16. "organizations:performance-view",
  17. ]
  18. def all_events_query(**kwargs):
  19. options = {
  20. "sort": ["-timestamp"],
  21. "field": ["title", "event.type", "project", "user.display", "timestamp"],
  22. "name": ["All Events"],
  23. }
  24. options.update(kwargs)
  25. return urlencode(options, doseq=True)
  26. def errors_query(**kwargs):
  27. options = {
  28. "sort": ["-title"],
  29. "name": ["Errors"],
  30. "field": ["title", "count(id)", "count_unique(user)", "project"],
  31. "query": ["event.type:error"],
  32. }
  33. options.update(kwargs)
  34. return urlencode(options, doseq=True)
  35. def transactions_query(**kwargs):
  36. options = {
  37. "sort": ["-count"],
  38. "name": ["Transactions"],
  39. "field": ["transaction", "project", "count()"],
  40. "statsPeriod": ["14d"],
  41. "query": ["event.type:transaction"],
  42. }
  43. options.update(kwargs)
  44. return urlencode(options, doseq=True)
  45. # Sorted by transactions to avoid sorting issues caused by storing events
  46. def transactions_sorted_query(**kwargs):
  47. options = {
  48. "sort": ["transaction"],
  49. "name": ["Transactions"],
  50. "field": ["transaction", "project", "count()"],
  51. "statsPeriod": ["14d"],
  52. "query": ["event.type:transaction"],
  53. }
  54. options.update(kwargs)
  55. return urlencode(options, doseq=True)
  56. def generate_transaction(trace=None, span=None):
  57. end_datetime = before_now(minutes=1)
  58. start_datetime = end_datetime - timedelta(milliseconds=500)
  59. event_data = load_data(
  60. "transaction",
  61. timestamp=end_datetime,
  62. start_timestamp=start_datetime,
  63. trace=trace,
  64. span_id=span,
  65. )
  66. event_data.update({"event_id": "a" * 32})
  67. # generate and build up span tree
  68. reference_span = event_data["spans"][0]
  69. parent_span_id = reference_span["parent_span_id"]
  70. span_tree_blueprint = {
  71. "a": {},
  72. "b": {"bb": {"bbb": {"bbbb": "bbbbb"}}},
  73. "c": {},
  74. "d": {},
  75. "e": {},
  76. }
  77. time_offsets = {
  78. "a": (timedelta(), timedelta(milliseconds=10)),
  79. "b": (timedelta(milliseconds=120), timedelta(milliseconds=250)),
  80. "bb": (timedelta(milliseconds=130), timedelta(milliseconds=10)),
  81. "bbb": (timedelta(milliseconds=140), timedelta(milliseconds=10)),
  82. "bbbb": (timedelta(milliseconds=150), timedelta(milliseconds=10)),
  83. "bbbbb": (timedelta(milliseconds=160), timedelta(milliseconds=90)),
  84. "c": (timedelta(milliseconds=260), timedelta(milliseconds=100)),
  85. "d": (timedelta(milliseconds=375), timedelta(milliseconds=50)),
  86. "e": (timedelta(milliseconds=400), timedelta(milliseconds=100)),
  87. }
  88. def build_span_tree(span_tree, spans, parent_span_id):
  89. for span_id, child in sorted(span_tree.items(), key=lambda item: item[0]):
  90. span = copy.deepcopy(reference_span)
  91. # non-leaf node span
  92. span["parent_span_id"] = parent_span_id.ljust(16, "0")
  93. span["span_id"] = span_id.ljust(16, "0")
  94. (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta()))
  95. span_start_time = start_datetime + start_delta
  96. span["start_timestamp"] = timestamp_format(span_start_time)
  97. span["timestamp"] = timestamp_format(span_start_time + span_length)
  98. spans.append(span)
  99. if isinstance(child, dict):
  100. spans = build_span_tree(child, spans, span_id)
  101. elif isinstance(child, str):
  102. parent_span_id = span_id
  103. span_id = child
  104. span = copy.deepcopy(reference_span)
  105. # leaf node span
  106. span["parent_span_id"] = parent_span_id.ljust(16, "0")
  107. span["span_id"] = span_id.ljust(16, "0")
  108. (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta()))
  109. span_start_time = start_datetime + start_delta
  110. span["start_timestamp"] = timestamp_format(span_start_time)
  111. span["timestamp"] = timestamp_format(span_start_time + span_length)
  112. spans.append(span)
  113. return spans
  114. event_data["spans"] = build_span_tree(span_tree_blueprint, [], parent_span_id)
  115. return event_data
  116. class OrganizationEventsV2Test(AcceptanceTestCase, SnubaTestCase):
  117. def setUp(self):
  118. super().setUp()
  119. self.user = self.create_user("foo@example.com", is_superuser=True)
  120. self.org = self.create_organization(name="Rowdy Tiger")
  121. self.team = self.create_team(organization=self.org, name="Mariachi Band")
  122. self.project = self.create_project(organization=self.org, teams=[self.team], name="Bengal")
  123. self.create_member(user=self.user, organization=self.org, role="owner", teams=[self.team])
  124. self.login_as(self.user)
  125. self.landing_path = f"/organizations/{self.org.slug}/discover/queries/"
  126. self.result_path = f"/organizations/{self.org.slug}/discover/results/"
  127. def wait_until_loaded(self):
  128. self.browser.wait_until_not('[data-test-id="loading-indicator"]')
  129. self.browser.wait_until_not('[data-test-id="loading-placeholder"]')
  130. def test_events_default_landing(self):
  131. with self.feature(FEATURE_NAMES):
  132. self.browser.get(self.landing_path)
  133. self.wait_until_loaded()
  134. self.browser.snapshot("events-v2 - default landing")
  135. def test_all_events_query_empty_state(self):
  136. with self.feature(FEATURE_NAMES):
  137. self.browser.get(self.result_path + "?" + all_events_query())
  138. self.wait_until_loaded()
  139. self.browser.snapshot("events-v2 - all events query - empty state")
  140. with self.feature(FEATURE_NAMES):
  141. # expect table to expand to the right when no tags are provided
  142. self.browser.get(self.result_path + "?" + all_events_query(tag=[]))
  143. self.wait_until_loaded()
  144. self.browser.snapshot("events-v2 - all events query - empty state - no tags")
  145. @patch("django.utils.timezone.now")
  146. def test_all_events_query(self, mock_now):
  147. now = before_now().replace(tzinfo=pytz.utc)
  148. mock_now.return_value = now
  149. min_ago = iso_format(now - timedelta(minutes=1))
  150. two_min_ago = iso_format(now - timedelta(minutes=2))
  151. self.store_event(
  152. data={
  153. "event_id": "a" * 32,
  154. "message": "oh no",
  155. "timestamp": min_ago,
  156. "fingerprint": ["group-1"],
  157. },
  158. project_id=self.project.id,
  159. assert_no_errors=False,
  160. )
  161. self.store_event(
  162. data={
  163. "event_id": "b" * 32,
  164. "message": "this is bad.",
  165. "timestamp": two_min_ago,
  166. "fingerprint": ["group-2"],
  167. "user": {
  168. "id": "123",
  169. "email": "someone@example.com",
  170. "username": "haveibeenpwned",
  171. "ip_address": "8.8.8.8",
  172. "name": "Someone",
  173. },
  174. },
  175. project_id=self.project.id,
  176. assert_no_errors=False,
  177. )
  178. self.wait_for_event_count(self.project.id, 2)
  179. with self.feature(FEATURE_NAMES):
  180. self.browser.get(self.result_path + "?" + all_events_query())
  181. self.wait_until_loaded()
  182. # This test is flakey in that we sometimes load this page before the event is processed
  183. # depend on pytest-retry to reload the page
  184. self.browser.wait_until('[data-test-id="grid-editable"] > tbody > tr:nth-child(2)')
  185. self.browser.snapshot("events-v2 - all events query - list")
  186. with self.feature(FEATURE_NAMES):
  187. # expect table to expand to the right when no tags are provided
  188. self.browser.get(self.result_path + "?" + all_events_query(tag=[]))
  189. self.wait_until_loaded()
  190. self.browser.wait_until('[data-test-id="grid-editable"] > tbody > tr:nth-child(2)')
  191. self.browser.snapshot("events-v2 - all events query - list - no tags")
  192. def test_errors_query_empty_state(self):
  193. with self.feature(FEATURE_NAMES):
  194. self.browser.get(self.result_path + "?" + errors_query())
  195. self.wait_until_loaded()
  196. self.browser.snapshot("events-v2 - errors query - empty state")
  197. self.browser.click_when_visible('[data-test-id="grid-edit-enable"]')
  198. self.browser.snapshot(
  199. "events-v2 - errors query - empty state - querybuilder - column edit state"
  200. )
  201. @patch("django.utils.timezone.now")
  202. def test_errors_query(self, mock_now):
  203. now = before_now().replace(tzinfo=pytz.utc)
  204. mock_now.return_value = now
  205. min_ago = iso_format(now - timedelta(minutes=1))
  206. self.store_event(
  207. data={
  208. "event_id": "a" * 32,
  209. "message": "oh no",
  210. "timestamp": min_ago,
  211. "fingerprint": ["group-1"],
  212. "type": "error",
  213. },
  214. project_id=self.project.id,
  215. assert_no_errors=False,
  216. )
  217. self.store_event(
  218. data={
  219. "event_id": "b" * 32,
  220. "message": "oh no",
  221. "timestamp": min_ago,
  222. "fingerprint": ["group-1"],
  223. "type": "error",
  224. },
  225. project_id=self.project.id,
  226. assert_no_errors=False,
  227. )
  228. self.store_event(
  229. data={
  230. "event_id": "c" * 32,
  231. "message": "this is bad.",
  232. "timestamp": min_ago,
  233. "fingerprint": ["group-2"],
  234. "type": "error",
  235. },
  236. project_id=self.project.id,
  237. assert_no_errors=False,
  238. )
  239. with self.feature(FEATURE_NAMES):
  240. self.browser.get(self.result_path + "?" + errors_query())
  241. self.wait_until_loaded()
  242. self.browser.snapshot("events-v2 - errors")
  243. def test_transactions_query_empty_state(self):
  244. with self.feature(FEATURE_NAMES):
  245. self.browser.get(self.result_path + "?" + transactions_query())
  246. self.wait_until_loaded()
  247. self.browser.snapshot("events-v2 - transactions query - empty state")
  248. with self.feature(FEATURE_NAMES):
  249. # expect table to expand to the right when no tags are provided
  250. self.browser.get(self.result_path + "?" + transactions_query(tag=[]))
  251. self.wait_until_loaded()
  252. self.browser.snapshot("events-v2 - transactions query - empty state - no tags")
  253. @patch("django.utils.timezone.now")
  254. def test_transactions_query(self, mock_now):
  255. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  256. event_data = generate_transaction()
  257. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
  258. with self.feature(FEATURE_NAMES):
  259. self.browser.get(self.result_path + "?" + transactions_query())
  260. self.wait_until_loaded()
  261. self.browser.wait_until_not(
  262. '[data-test-id="grid-editable"] [data-test-id="empty-state"]', timeout=2
  263. )
  264. self.browser.snapshot("events-v2 - transactions query - list")
  265. @patch("django.utils.timezone.now")
  266. def test_event_detail_view_from_all_events(self, mock_now):
  267. now = before_now().replace(tzinfo=pytz.utc)
  268. mock_now.return_value = now
  269. min_ago = iso_format(now - timedelta(minutes=1))
  270. event_data = load_data("python")
  271. event_data.update(
  272. {
  273. "event_id": "a" * 32,
  274. "timestamp": min_ago,
  275. "received": min_ago,
  276. "fingerprint": ["group-1"],
  277. }
  278. )
  279. if "contexts" not in event_data:
  280. event_data["contexts"] = {}
  281. event_data["contexts"]["trace"] = {
  282. "type": "trace",
  283. "trace_id": "a" * 32,
  284. "span_id": "b" * 16,
  285. }
  286. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=False)
  287. with self.feature(FEATURE_NAMES):
  288. # Get the list page.
  289. self.browser.get(self.result_path + "?" + all_events_query())
  290. self.wait_until_loaded()
  291. # View Event
  292. self.browser.elements('[data-test-id="view-event"]')[0].click()
  293. self.wait_until_loaded()
  294. # header = self.browser.element('[data-test-id="event-header"] div div span')
  295. # assert event_data["message"] in header.text
  296. self.browser.snapshot("events-v2 - single error details view")
  297. @patch("django.utils.timezone.now")
  298. def test_event_detail_view_from_errors_view(self, mock_now):
  299. now = before_now().replace(tzinfo=pytz.utc)
  300. mock_now.return_value = now
  301. event_data = load_data("javascript")
  302. event_data.update(
  303. {
  304. "timestamp": iso_format(now - timedelta(minutes=5)),
  305. "event_id": "d" * 32,
  306. "fingerprint": ["group-1"],
  307. }
  308. )
  309. event_data["contexts"]["trace"] = {
  310. "type": "trace",
  311. "trace_id": "a" * 32,
  312. "span_id": "b" * 16,
  313. }
  314. self.store_event(data=event_data, project_id=self.project.id)
  315. self.wait_for_event_count(self.project.id, 1)
  316. with self.feature(FEATURE_NAMES):
  317. # Get the list page
  318. self.browser.get(self.result_path + "?" + errors_query() + "&statsPeriod=24h")
  319. self.wait_until_loaded()
  320. # Open the stack
  321. self.browser.element('[data-test-id="open-group"]').click()
  322. self.wait_until_loaded()
  323. # View Event
  324. self.browser.elements('[data-test-id="view-event"]')[0].click()
  325. self.wait_until_loaded()
  326. self.browser.snapshot("events-v2 - error event detail view")
  327. @patch("django.utils.timezone.now")
  328. def test_event_detail_view_from_transactions_query(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. # Create a child event that is linked to the parent so we have coverage
  333. # of traversal buttons.
  334. child_event = generate_transaction(
  335. trace=event_data["contexts"]["trace"]["trace_id"], span="bc" * 8
  336. )
  337. child_event["event_id"] = "b" * 32
  338. child_event["contexts"]["trace"]["parent_span_id"] = event_data["spans"][4]["span_id"]
  339. child_event["transaction"] = "z-child-transaction"
  340. child_event["spans"] = child_event["spans"][0:3]
  341. self.store_event(data=child_event, project_id=self.project.id, assert_no_errors=True)
  342. with self.feature(FEATURE_NAMES):
  343. # Get the list page
  344. self.browser.get(self.result_path + "?" + transactions_sorted_query())
  345. self.wait_until_loaded()
  346. # Open the stack
  347. self.browser.elements('[data-test-id="open-group"]')[0].click()
  348. self.wait_until_loaded()
  349. # View Event
  350. self.browser.elements('[data-test-id="view-event"]')[0].click()
  351. self.wait_until_loaded()
  352. self.browser.snapshot("events-v2 - transactions event with auto-grouped spans")
  353. # Expand auto-grouped spans
  354. self.browser.element('[data-test-id="span-row-5"]').click()
  355. # Open a span detail so we can check the search by trace link.
  356. # Click on the 6th one as a missing instrumentation span is inserted.
  357. self.browser.element('[data-test-id="span-row-7"]').click()
  358. # Wait until the child event loads.
  359. child_button = '[data-test-id="view-child-transaction"]'
  360. self.browser.wait_until(child_button)
  361. self.browser.snapshot("events-v2 - transactions event detail view")
  362. # Click on the child transaction.
  363. self.browser.click(child_button)
  364. self.wait_until_loaded()
  365. @patch("django.utils.timezone.now")
  366. def test_event_detail_view_from_transactions_query_siblings(self, mock_now):
  367. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  368. event_data = generate_transaction(trace="a" * 32, span="ab" * 8)
  369. # Arranges sibling spans to be autogrouped in a way that will cover many edgecases
  370. last_span = copy.deepcopy(event_data["spans"][-1])
  371. for i in range(5):
  372. clone = copy.deepcopy(last_span)
  373. # If range > 9 this might no longer work because of constraints on span_id (hex 16)
  374. clone["span_id"] = (str("ac" * 6) + str(i)).ljust(16, "0")
  375. event_data["spans"].append(clone)
  376. combo_breaker_span = copy.deepcopy(last_span)
  377. combo_breaker_span["span_id"] = (str("af" * 6)).ljust(16, "0")
  378. combo_breaker_span["op"] = "combo.breaker"
  379. event_data["spans"].append(combo_breaker_span)
  380. for i in range(5):
  381. clone = copy.deepcopy(last_span)
  382. clone["op"] = "django.middleware"
  383. clone["span_id"] = (str("de" * 6) + str(i)).ljust(16, "0")
  384. event_data["spans"].append(clone)
  385. for i in range(5):
  386. clone = copy.deepcopy(last_span)
  387. clone["op"] = "http"
  388. clone["description"] = "test"
  389. clone["span_id"] = (str("bd" * 6) + str(i)).ljust(16, "0")
  390. event_data["spans"].append(clone)
  391. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
  392. # Create a child event that is linked to the parent so we have coverage
  393. # of traversal buttons.
  394. child_event = generate_transaction(
  395. trace=event_data["contexts"]["trace"]["trace_id"], span="bc" * 8
  396. )
  397. child_event["event_id"] = "b" * 32
  398. child_event["contexts"]["trace"]["parent_span_id"] = event_data["spans"][4]["span_id"]
  399. child_event["transaction"] = "z-child-transaction"
  400. child_event["spans"] = child_event["spans"][0:3]
  401. self.store_event(data=child_event, project_id=self.project.id, assert_no_errors=True)
  402. with self.feature(FEATURE_NAMES + ["organizations:performance-autogroup-sibling-spans"]):
  403. # Get the list page
  404. self.browser.get(self.result_path + "?" + transactions_sorted_query())
  405. self.wait_until_loaded()
  406. # Open the stack
  407. self.browser.elements('[data-test-id="open-group"]')[0].click()
  408. self.wait_until_loaded()
  409. # View Event
  410. self.browser.elements('[data-test-id="view-event"]')[0].click()
  411. self.wait_until_loaded()
  412. self.browser.snapshot(
  413. "events-v2 - transactions event with descendant and sibling auto-grouped spans"
  414. )
  415. # Expand auto-grouped descendant spans
  416. self.browser.element('[data-test-id="span-row-5"]').click()
  417. # Expand all autogrouped rows
  418. self.browser.element('[data-test-id="span-row-9"]').click()
  419. self.browser.element('[data-test-id="span-row-18"]').click()
  420. self.browser.element('[data-test-id="span-row-23"]').click()
  421. self.browser.snapshot(
  422. "events-v2 - transactions event with expanded descendant and sibling auto-grouped spans"
  423. )
  424. # Click to collapse all of these spans back into autogroups, we expect the span tree to look like it did initially
  425. first_row = self.browser.element('[data-test-id="span-row-23"]')
  426. first_row.find_element(By.CSS_SELECTOR, "a").click()
  427. second_row = self.browser.element('[data-test-id="span-row-18"]')
  428. second_row.find_element(By.CSS_SELECTOR, "a").click()
  429. third_row = self.browser.element('[data-test-id="span-row-9"]')
  430. third_row.find_element(By.CSS_SELECTOR, "a").click()
  431. self.browser.snapshot(
  432. "events-v2 - transactions event after regrouping expanded sibling auto-grouped spans"
  433. )
  434. @patch("django.utils.timezone.now")
  435. def test_transaction_event_detail_view_ops_filtering(self, mock_now):
  436. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  437. event_data = generate_transaction(trace="a" * 32, span="ab" * 8)
  438. self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
  439. with self.feature(FEATURE_NAMES):
  440. # Get the list page
  441. self.browser.get(self.result_path + "?" + transactions_query())
  442. self.wait_until_loaded()
  443. # Open the stack
  444. self.browser.elements('[data-test-id="open-group"]')[0].click()
  445. self.wait_until_loaded()
  446. # View Event
  447. self.browser.elements('[data-test-id="view-event"]')[0].click()
  448. self.wait_until_loaded()
  449. # Interact with ops filter dropdown
  450. self.browser.elements('[aria-haspopup="listbox"]')[0].click()
  451. # select django.middleware
  452. self.browser.elements('[data-test-id="django.middleware"]')[0].click()
  453. self.browser.snapshot("events-v2 - transactions event detail view - ops filtering")
  454. def test_create_saved_query(self):
  455. # Simulate a custom query
  456. query = {"field": ["project.id", "count()"], "query": "event.type:error"}
  457. query_name = "A new custom query"
  458. with self.feature(FEATURE_NAMES):
  459. # Go directly to the query builder view
  460. self.browser.get(self.result_path + "?" + urlencode(query, doseq=True))
  461. self.wait_until_loaded()
  462. # Open the save as drawer
  463. self.browser.element('[aria-label="Save as"]').click()
  464. # Fill out name and submit form.
  465. self.browser.element('input[name="query_name"]').send_keys(query_name)
  466. self.browser.element('[aria-label="Save for Org"]').click()
  467. self.browser.wait_until(f'[data-test-id="discover2-query-name-{query_name}"]')
  468. # Page title should update.
  469. editable_text_label = self.browser.element('[data-test-id="editable-text-label"]').text
  470. assert editable_text_label == query_name
  471. # Saved query should exist.
  472. assert DiscoverSavedQuery.objects.filter(name=query_name).exists()
  473. def test_view_and_rename_saved_query(self):
  474. # Create saved query to rename
  475. query = DiscoverSavedQuery.objects.create(
  476. name="Custom query",
  477. organization=self.org,
  478. version=2,
  479. query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
  480. )
  481. with self.feature(FEATURE_NAMES):
  482. # View the query list
  483. self.browser.get(self.landing_path)
  484. self.wait_until_loaded()
  485. # Look at the results for our query.
  486. self.browser.element(f'[data-test-id="card-{query.name}"]').click()
  487. self.wait_until_loaded()
  488. self.browser.element('[data-test-id="editable-text-label"]').click()
  489. self.browser.wait_until('[data-test-id="editable-text-input"]')
  490. editable_text_input = self.browser.element('[data-test-id="editable-text-input"] input')
  491. editable_text_input.click()
  492. editable_text_input.send_keys(Keys.END + "updated!")
  493. # Move focus somewhere else to trigger a blur and update the query
  494. self.browser.element("table").click()
  495. self.browser.wait_until('[data-test-id="editable-text-label"]')
  496. new_name = "Custom queryupdated!"
  497. # new_card_selector = f'div[name="discover2-query-name"][value="{new_name}"]'
  498. # self.browser.wait_until(new_card_selector)
  499. self.browser.wait_until(f'[data-test-id="discover2-query-name-{new_name}"]')
  500. # Assert the name was updated.
  501. assert DiscoverSavedQuery.objects.filter(name=new_name).exists()
  502. def test_delete_saved_query(self):
  503. # Create saved query with ORM
  504. query = DiscoverSavedQuery.objects.create(
  505. name="Custom query",
  506. organization=self.org,
  507. version=2,
  508. query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
  509. )
  510. with self.feature(FEATURE_NAMES):
  511. # View the query list
  512. self.browser.get(self.landing_path)
  513. self.wait_until_loaded()
  514. # Get the card with the new query
  515. card_selector = f'[data-test-id="card-{query.name}"]'
  516. card = self.browser.element(card_selector)
  517. # Open the context menu
  518. card.find_element(by=By.CSS_SELECTOR, value='[data-test-id="menu-trigger"]').click()
  519. # Delete the query
  520. card.find_element(by=By.CSS_SELECTOR, value='[data-test-id="delete"]').click()
  521. # Wait for card to clear
  522. self.browser.wait_until_not(card_selector)
  523. assert DiscoverSavedQuery.objects.filter(name=query.name).exists() is False
  524. def test_duplicate_query(self):
  525. # Create saved query with ORM
  526. query = DiscoverSavedQuery.objects.create(
  527. name="Custom query",
  528. organization=self.org,
  529. version=2,
  530. query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
  531. )
  532. with self.feature(FEATURE_NAMES):
  533. # View the query list
  534. self.browser.get(self.landing_path)
  535. self.wait_until_loaded()
  536. # Get the card with the new query
  537. card_selector = f'[data-test-id="card-{query.name}"]'
  538. card = self.browser.element(card_selector)
  539. # Open the context menu, and duplicate
  540. card.find_element(by=By.CSS_SELECTOR, value='[data-test-id="menu-trigger"]').click()
  541. card.find_element(by=By.CSS_SELECTOR, value='[data-test-id="duplicate"]').click()
  542. duplicate_name = f"{query.name} copy"
  543. # Reload the page
  544. self.browser.get(self.landing_path)
  545. # Wait for new element to show up.
  546. self.browser.element(f'[data-test-id="card-{duplicate_name}"]')
  547. # Assert the new query exists and has 'copy' added to the name.
  548. assert DiscoverSavedQuery.objects.filter(name=duplicate_name).exists()
  549. @pytest.mark.skip(reason="causing timeouts in github actions and travis")
  550. @patch("django.utils.timezone.now")
  551. def test_drilldown_result(self, mock_now):
  552. now = before_now().replace(tzinfo=pytz.utc)
  553. mock_now.return_value = now
  554. min_ago = iso_format(now - timedelta(minutes=1))
  555. events = (
  556. ("a" * 32, "oh no", "group-1"),
  557. ("b" * 32, "oh no", "group-1"),
  558. ("c" * 32, "this is bad", "group-2"),
  559. )
  560. for event in events:
  561. self.store_event(
  562. data={
  563. "event_id": event[0],
  564. "message": event[1],
  565. "timestamp": min_ago,
  566. "fingerprint": [event[2]],
  567. "type": "error",
  568. },
  569. project_id=self.project.id,
  570. )
  571. query = {"field": ["message", "project", "count()"], "query": "event.type:error"}
  572. with self.feature(FEATURE_NAMES):
  573. # Go directly to the query builder view
  574. self.browser.get(self.result_path + "?" + urlencode(query, doseq=True))
  575. self.wait_until_loaded()
  576. # Click the first drilldown
  577. self.browser.element('[data-test-id="expand-count"]').click()
  578. self.wait_until_loaded()
  579. assert self.browser.element_exists_by_test_id("grid-editable"), "table should exist."
  580. headers = self.browser.elements('[data-test-id="grid-editable"] thead th')
  581. expected = ["", "MESSAGE", "PROJECT", "ID"]
  582. actual = [header.text for header in headers]
  583. assert expected == actual
  584. @pytest.mark.skip(reason="not done")
  585. @patch("django.utils.timezone.now")
  586. def test_usage(self, mock_now):
  587. mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
  588. # TODO: load events
  589. # go to landing
  590. # go to a precanned query
  591. # save query 1
  592. # add environment column
  593. # update query
  594. # add condition from facet map
  595. # delete a column
  596. # click and drag a column
  597. # save as query 2
  598. # load save query 1
  599. # sort column
  600. # update query
  601. # delete save query 1