test_organization_events_v2.py 27 KB

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