123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 |
- import copy
- from datetime import timedelta
- from unittest.mock import patch
- from urllib.parse import urlencode
- import pytest
- import pytz
- from selenium.webdriver.common.keys import Keys
- from sentry.discover.models import DiscoverSavedQuery
- from sentry.testutils import AcceptanceTestCase, SnubaTestCase
- from sentry.testutils.helpers.datetime import before_now, iso_format, timestamp_format
- from sentry.utils.samples import load_data
- FEATURE_NAMES = [
- "organizations:discover-basic",
- "organizations:discover-query",
- "organizations:performance-view",
- ]
- def all_events_query(**kwargs):
- options = {
- "sort": ["-timestamp"],
- "field": ["title", "event.type", "project", "user.display", "timestamp"],
- "name": ["All Events"],
- }
- options.update(kwargs)
- return urlencode(options, doseq=True)
- def errors_query(**kwargs):
- options = {
- "sort": ["-title"],
- "name": ["Errors"],
- "field": ["title", "count(id)", "count_unique(user)", "project"],
- "query": ["event.type:error"],
- }
- options.update(kwargs)
- return urlencode(options, doseq=True)
- def transactions_query(**kwargs):
- options = {
- "sort": ["-count"],
- "name": ["Transactions"],
- "field": ["transaction", "project", "count()"],
- "statsPeriod": ["14d"],
- "query": ["event.type:transaction"],
- }
- options.update(kwargs)
- return urlencode(options, doseq=True)
- def generate_transaction(trace=None, span=None):
- end_datetime = before_now(minutes=1)
- start_datetime = end_datetime - timedelta(milliseconds=500)
- event_data = load_data(
- "transaction",
- timestamp=end_datetime,
- start_timestamp=start_datetime,
- trace=trace,
- span_id=span,
- )
- event_data.update({"event_id": "a" * 32})
- # generate and build up span tree
- reference_span = event_data["spans"][0]
- parent_span_id = reference_span["parent_span_id"]
- span_tree_blueprint = {
- "a": {},
- "b": {"bb": {"bbb": {"bbbb": "bbbbb"}}},
- "c": {},
- "d": {},
- "e": {},
- }
- time_offsets = {
- "a": (timedelta(), timedelta(milliseconds=10)),
- "b": (timedelta(milliseconds=120), timedelta(milliseconds=250)),
- "bb": (timedelta(milliseconds=130), timedelta(milliseconds=10)),
- "bbb": (timedelta(milliseconds=140), timedelta(milliseconds=10)),
- "bbbb": (timedelta(milliseconds=150), timedelta(milliseconds=10)),
- "bbbbb": (timedelta(milliseconds=160), timedelta(milliseconds=90)),
- "c": (timedelta(milliseconds=260), timedelta(milliseconds=100)),
- "d": (timedelta(milliseconds=375), timedelta(milliseconds=50)),
- "e": (timedelta(milliseconds=400), timedelta(milliseconds=100)),
- }
- def build_span_tree(span_tree, spans, parent_span_id):
- for span_id, child in sorted(span_tree.items(), key=lambda item: item[0]):
- span = copy.deepcopy(reference_span)
- # non-leaf node span
- span["parent_span_id"] = parent_span_id.ljust(16, "0")
- span["span_id"] = span_id.ljust(16, "0")
- (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta()))
- span_start_time = start_datetime + start_delta
- span["start_timestamp"] = timestamp_format(span_start_time)
- span["timestamp"] = timestamp_format(span_start_time + span_length)
- spans.append(span)
- if isinstance(child, dict):
- spans = build_span_tree(child, spans, span_id)
- elif isinstance(child, str):
- parent_span_id = span_id
- span_id = child
- span = copy.deepcopy(reference_span)
- # leaf node span
- span["parent_span_id"] = parent_span_id.ljust(16, "0")
- span["span_id"] = span_id.ljust(16, "0")
- (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta()))
- span_start_time = start_datetime + start_delta
- span["start_timestamp"] = timestamp_format(span_start_time)
- span["timestamp"] = timestamp_format(span_start_time + span_length)
- spans.append(span)
- return spans
- event_data["spans"] = build_span_tree(span_tree_blueprint, [], parent_span_id)
- return event_data
- class OrganizationEventsV2Test(AcceptanceTestCase, SnubaTestCase):
- def setUp(self):
- super().setUp()
- self.user = self.create_user("foo@example.com", is_superuser=True)
- self.org = self.create_organization(name="Rowdy Tiger")
- self.team = self.create_team(organization=self.org, name="Mariachi Band")
- self.project = self.create_project(organization=self.org, teams=[self.team], name="Bengal")
- self.create_member(user=self.user, organization=self.org, role="owner", teams=[self.team])
- self.login_as(self.user)
- self.landing_path = f"/organizations/{self.org.slug}/discover/queries/"
- self.result_path = f"/organizations/{self.org.slug}/discover/results/"
- def wait_until_loaded(self):
- self.browser.wait_until_not('[data-test-id="loading-indicator"]')
- self.browser.wait_until_not('[data-test-id="loading-placeholder"]')
- def test_events_default_landing(self):
- with self.feature(FEATURE_NAMES):
- self.browser.get(self.landing_path)
- self.wait_until_loaded()
- self.browser.snapshot("events-v2 - default landing")
- def test_all_events_query_empty_state(self):
- with self.feature(FEATURE_NAMES):
- self.browser.get(self.result_path + "?" + all_events_query())
- self.wait_until_loaded()
- self.browser.snapshot("events-v2 - all events query - empty state")
- with self.feature(FEATURE_NAMES):
- # expect table to expand to the right when no tags are provided
- self.browser.get(self.result_path + "?" + all_events_query(tag=[]))
- self.wait_until_loaded()
- self.browser.snapshot("events-v2 - all events query - empty state - no tags")
- @patch("django.utils.timezone.now")
- def test_all_events_query(self, mock_now):
- now = before_now().replace(tzinfo=pytz.utc)
- mock_now.return_value = now
- min_ago = iso_format(now - timedelta(minutes=1))
- two_min_ago = iso_format(now - timedelta(minutes=2))
- self.store_event(
- data={
- "event_id": "a" * 32,
- "message": "oh no",
- "timestamp": min_ago,
- "fingerprint": ["group-1"],
- },
- project_id=self.project.id,
- assert_no_errors=False,
- )
- self.store_event(
- data={
- "event_id": "b" * 32,
- "message": "this is bad.",
- "timestamp": two_min_ago,
- "fingerprint": ["group-2"],
- "user": {
- "id": "123",
- "email": "someone@example.com",
- "username": "haveibeenpwned",
- "ip_address": "8.8.8.8",
- "name": "Someone",
- },
- },
- project_id=self.project.id,
- assert_no_errors=False,
- )
- self.wait_for_event_count(self.project.id, 2)
- with self.feature(FEATURE_NAMES):
- self.browser.get(self.result_path + "?" + all_events_query())
- self.wait_until_loaded()
- # This test is flakey in that we sometimes load this page before the event is processed
- # depend on pytest-retry to reload the page
- self.browser.wait_until('[data-test-id="grid-editable"] > tbody > tr:nth-child(2)')
- self.browser.snapshot("events-v2 - all events query - list")
- with self.feature(FEATURE_NAMES):
- # expect table to expand to the right when no tags are provided
- self.browser.get(self.result_path + "?" + all_events_query(tag=[]))
- self.wait_until_loaded()
- self.browser.wait_until('[data-test-id="grid-editable"] > tbody > tr:nth-child(2)')
- self.browser.snapshot("events-v2 - all events query - list - no tags")
- def test_errors_query_empty_state(self):
- with self.feature(FEATURE_NAMES):
- self.browser.get(self.result_path + "?" + errors_query())
- self.wait_until_loaded()
- self.browser.snapshot("events-v2 - errors query - empty state")
- self.browser.click_when_visible('[data-test-id="grid-edit-enable"]')
- self.browser.snapshot(
- "events-v2 - errors query - empty state - querybuilder - column edit state"
- )
- @patch("django.utils.timezone.now")
- def test_errors_query(self, mock_now):
- now = before_now().replace(tzinfo=pytz.utc)
- mock_now.return_value = now
- min_ago = iso_format(now - timedelta(minutes=1))
- self.store_event(
- data={
- "event_id": "a" * 32,
- "message": "oh no",
- "timestamp": min_ago,
- "fingerprint": ["group-1"],
- "type": "error",
- },
- project_id=self.project.id,
- assert_no_errors=False,
- )
- self.store_event(
- data={
- "event_id": "b" * 32,
- "message": "oh no",
- "timestamp": min_ago,
- "fingerprint": ["group-1"],
- "type": "error",
- },
- project_id=self.project.id,
- assert_no_errors=False,
- )
- self.store_event(
- data={
- "event_id": "c" * 32,
- "message": "this is bad.",
- "timestamp": min_ago,
- "fingerprint": ["group-2"],
- "type": "error",
- },
- project_id=self.project.id,
- assert_no_errors=False,
- )
- with self.feature(FEATURE_NAMES):
- self.browser.get(self.result_path + "?" + errors_query())
- self.wait_until_loaded()
- self.browser.snapshot("events-v2 - errors")
- def test_transactions_query_empty_state(self):
- with self.feature(FEATURE_NAMES):
- self.browser.get(self.result_path + "?" + transactions_query())
- self.wait_until_loaded()
- self.browser.snapshot("events-v2 - transactions query - empty state")
- with self.feature(FEATURE_NAMES):
- # expect table to expand to the right when no tags are provided
- self.browser.get(self.result_path + "?" + transactions_query(tag=[]))
- self.wait_until_loaded()
- self.browser.snapshot("events-v2 - transactions query - empty state - no tags")
- @patch("django.utils.timezone.now")
- def test_transactions_query(self, mock_now):
- mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
- event_data = generate_transaction()
- self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
- with self.feature(FEATURE_NAMES):
- self.browser.get(self.result_path + "?" + transactions_query())
- self.wait_until_loaded()
- self.browser.wait_until_not(
- '[data-test-id="grid-editable"] [data-test-id="empty-state"]', timeout=2
- )
- self.browser.snapshot("events-v2 - transactions query - list")
- @patch("django.utils.timezone.now")
- def test_event_detail_view_from_all_events(self, mock_now):
- now = before_now().replace(tzinfo=pytz.utc)
- mock_now.return_value = now
- min_ago = iso_format(now - timedelta(minutes=1))
- event_data = load_data("python")
- event_data.update(
- {
- "event_id": "a" * 32,
- "timestamp": min_ago,
- "received": min_ago,
- "fingerprint": ["group-1"],
- }
- )
- if "contexts" not in event_data:
- event_data["contexts"] = {}
- event_data["contexts"]["trace"] = {
- "type": "trace",
- "trace_id": "a" * 32,
- "span_id": "b" * 16,
- }
- self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=False)
- with self.feature(FEATURE_NAMES):
- # Get the list page.
- self.browser.get(self.result_path + "?" + all_events_query())
- self.wait_until_loaded()
- # View Event
- self.browser.elements('[data-test-id="view-event"]')[0].click()
- self.wait_until_loaded()
- # header = self.browser.element('[data-test-id="event-header"] div div span')
- # assert event_data["message"] in header.text
- self.browser.snapshot("events-v2 - single error details view")
- @patch("django.utils.timezone.now")
- def test_event_detail_view_from_errors_view(self, mock_now):
- now = before_now().replace(tzinfo=pytz.utc)
- mock_now.return_value = now
- event_data = load_data("javascript")
- event_data.update(
- {
- "timestamp": iso_format(now - timedelta(minutes=5)),
- "event_id": "d" * 32,
- "fingerprint": ["group-1"],
- }
- )
- event_data["contexts"]["trace"] = {
- "type": "trace",
- "trace_id": "a" * 32,
- "span_id": "b" * 16,
- }
- self.store_event(data=event_data, project_id=self.project.id)
- self.wait_for_event_count(self.project.id, 1)
- with self.feature(FEATURE_NAMES):
- # Get the list page
- self.browser.get(self.result_path + "?" + errors_query() + "&statsPeriod=24h")
- self.wait_until_loaded()
- # Open the stack
- self.browser.element('[data-test-id="open-group"]').click()
- self.wait_until_loaded()
- # View Event
- self.browser.elements('[data-test-id="view-event"]')[0].click()
- self.wait_until_loaded()
- self.browser.snapshot("events-v2 - error event detail view")
- @patch("django.utils.timezone.now")
- def test_event_detail_view_from_transactions_query(self, mock_now):
- mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
- event_data = generate_transaction(trace="a" * 32, span="ab" * 8)
- self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
- # Create a child event that is linked to the parent so we have coverage
- # of traversal buttons.
- child_event = generate_transaction(
- trace=event_data["contexts"]["trace"]["trace_id"], span="bc" * 8
- )
- child_event["event_id"] = "b" * 32
- child_event["contexts"]["trace"]["parent_span_id"] = event_data["spans"][4]["span_id"]
- child_event["transaction"] = "z-child-transaction"
- child_event["spans"] = child_event["spans"][0:3]
- self.store_event(data=child_event, project_id=self.project.id, assert_no_errors=True)
- with self.feature(FEATURE_NAMES):
- # Get the list page
- self.browser.get(self.result_path + "?" + transactions_query())
- self.wait_until_loaded()
- # Open the stack
- self.browser.elements('[data-test-id="open-group"]')[0].click()
- self.wait_until_loaded()
- # View Event
- self.browser.elements('[data-test-id="view-event"]')[0].click()
- self.wait_until_loaded()
- self.browser.snapshot("events-v2 - transactions event with auto-grouped spans")
- # Expand auto-grouped spans
- self.browser.elements('[data-test-id="span-row"]')[4].click()
- # Open a span detail so we can check the search by trace link.
- # Click on the 6th one as a missing instrumentation span is inserted.
- self.browser.elements('[data-test-id="span-row"]')[6].click()
- # Wait until the child event loads.
- child_button = '[data-test-id="view-child-transaction"]'
- self.browser.wait_until(child_button)
- self.browser.snapshot("events-v2 - transactions event detail view")
- # Click on the child transaction.
- self.browser.click(child_button)
- self.wait_until_loaded()
- @patch("django.utils.timezone.now")
- def test_transaction_event_detail_view_ops_filtering(self, mock_now):
- mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
- event_data = generate_transaction(trace="a" * 32, span="ab" * 8)
- self.store_event(data=event_data, project_id=self.project.id, assert_no_errors=True)
- with self.feature(FEATURE_NAMES):
- # Get the list page
- self.browser.get(self.result_path + "?" + transactions_query())
- self.wait_until_loaded()
- # Open the stack
- self.browser.elements('[data-test-id="open-group"]')[0].click()
- self.wait_until_loaded()
- # View Event
- self.browser.elements('[data-test-id="view-event"]')[0].click()
- self.wait_until_loaded()
- # Interact with ops filter dropdown
- self.browser.elements('[data-test-id="filter-button"]')[0].click()
- # select all ops
- self.browser.elements(
- '[data-test-id="op-filter-dropdown"] [data-test-id="checkbox-fancy"]'
- )[0].click()
- # un-select django.middleware
- self.browser.elements(
- '[data-test-id="op-filter-dropdown"] [data-test-id="checkbox-fancy"]'
- )[1].click()
- self.browser.snapshot("events-v2 - transactions event detail view - ops filtering")
- def test_create_saved_query(self):
- # Simulate a custom query
- query = {"field": ["project.id", "count()"], "query": "event.type:error"}
- query_name = "A new custom query"
- with self.feature(FEATURE_NAMES):
- # Go directly to the query builder view
- self.browser.get(self.result_path + "?" + urlencode(query, doseq=True))
- self.wait_until_loaded()
- # Open the save as drawer
- self.browser.element('[aria-label="Save as"]').click()
- # Fill out name and submit form.
- self.browser.element('input[name="query_name"]').send_keys(query_name)
- self.browser.element('[aria-label="Save for Org"]').click()
- self.browser.wait_until(f'[data-test-id="discover2-query-name-{query_name}"]')
- # Page title should update.
- editable_text_label = self.browser.element('[data-test-id="editable-text-label"]').text
- assert editable_text_label == query_name
- # Saved query should exist.
- assert DiscoverSavedQuery.objects.filter(name=query_name).exists()
- def test_view_and_rename_saved_query(self):
- # Create saved query to rename
- query = DiscoverSavedQuery.objects.create(
- name="Custom query",
- organization=self.org,
- version=2,
- query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
- )
- with self.feature(FEATURE_NAMES):
- # View the query list
- self.browser.get(self.landing_path)
- self.wait_until_loaded()
- # Look at the results for our query.
- self.browser.element(f'[data-test-id="card-{query.name}"]').click()
- self.wait_until_loaded()
- self.browser.element('[data-test-id="editable-text-label"]').click()
- self.browser.wait_until('[data-test-id="editable-text-input"]')
- editable_text_input = self.browser.element('[data-test-id="editable-text-input"] input')
- editable_text_input.click()
- editable_text_input.send_keys(Keys.END + "updated!")
- # Move focus somewhere else to trigger a blur and update the query
- self.browser.element("table").click()
- self.browser.wait_until('[data-test-id="editable-text-label"]')
- new_name = "Custom queryupdated!"
- # new_card_selector = f'div[name="discover2-query-name"][value="{new_name}"]'
- # self.browser.wait_until(new_card_selector)
- self.browser.wait_until(f'[data-test-id="discover2-query-name-{new_name}"]')
- # Assert the name was updated.
- assert DiscoverSavedQuery.objects.filter(name=new_name).exists()
- def test_delete_saved_query(self):
- # Create saved query with ORM
- query = DiscoverSavedQuery.objects.create(
- name="Custom query",
- organization=self.org,
- version=2,
- query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
- )
- with self.feature(FEATURE_NAMES):
- # View the query list
- self.browser.get(self.landing_path)
- self.wait_until_loaded()
- # Get the card with the new query
- card_selector = f'[data-test-id="card-{query.name}"]'
- card = self.browser.element(card_selector)
- # Open the context menu
- card.find_element_by_css_selector('[data-test-id="menu-trigger"]').click()
- # Delete the query
- card.find_element_by_css_selector('[data-test-id="delete"]').click()
- # Wait for card to clear
- self.browser.wait_until_not(card_selector)
- assert DiscoverSavedQuery.objects.filter(name=query.name).exists() is False
- def test_duplicate_query(self):
- # Create saved query with ORM
- query = DiscoverSavedQuery.objects.create(
- name="Custom query",
- organization=self.org,
- version=2,
- query={"fields": ["title", "project.id", "count()"], "query": "event.type:error"},
- )
- with self.feature(FEATURE_NAMES):
- # View the query list
- self.browser.get(self.landing_path)
- self.wait_until_loaded()
- # Get the card with the new query
- card_selector = f'[data-test-id="card-{query.name}"]'
- card = self.browser.element(card_selector)
- # Open the context menu, and duplicate
- card.find_element_by_css_selector('[data-test-id="menu-trigger"]').click()
- card.find_element_by_css_selector('[data-test-id="duplicate"]').click()
- duplicate_name = f"{query.name} copy"
- # Reload the page
- self.browser.get(self.landing_path)
- # Wait for new element to show up.
- self.browser.element(f'[data-test-id="card-{duplicate_name}"]')
- # Assert the new query exists and has 'copy' added to the name.
- assert DiscoverSavedQuery.objects.filter(name=duplicate_name).exists()
- @pytest.mark.skip(reason="causing timeouts in github actions and travis")
- @patch("django.utils.timezone.now")
- def test_drilldown_result(self, mock_now):
- now = before_now().replace(tzinfo=pytz.utc)
- mock_now.return_value = now
- min_ago = iso_format(now - timedelta(minutes=1))
- events = (
- ("a" * 32, "oh no", "group-1"),
- ("b" * 32, "oh no", "group-1"),
- ("c" * 32, "this is bad", "group-2"),
- )
- for event in events:
- self.store_event(
- data={
- "event_id": event[0],
- "message": event[1],
- "timestamp": min_ago,
- "fingerprint": [event[2]],
- "type": "error",
- },
- project_id=self.project.id,
- )
- query = {"field": ["message", "project", "count()"], "query": "event.type:error"}
- with self.feature(FEATURE_NAMES):
- # Go directly to the query builder view
- self.browser.get(self.result_path + "?" + urlencode(query, doseq=True))
- self.wait_until_loaded()
- # Click the first drilldown
- self.browser.element('[data-test-id="expand-count"]').click()
- self.wait_until_loaded()
- assert self.browser.element_exists_by_test_id("grid-editable"), "table should exist."
- headers = self.browser.elements('[data-test-id="grid-editable"] thead th')
- expected = ["", "MESSAGE", "PROJECT", "ID"]
- actual = [header.text for header in headers]
- assert expected == actual
- @pytest.mark.skip(reason="not done")
- @patch("django.utils.timezone.now")
- def test_usage(self, mock_now):
- mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
- # TODO: load events
- # go to landing
- # go to a precanned query
- # save query 1
- # add environment column
- # update query
- # add condition from facet map
- # delete a column
- # click and drag a column
- # save as query 2
- # load save query 1
- # sort column
- # update query
- # delete save query 1
|