123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- from datetime import timedelta
- import pytest
- from django.urls import NoReverseMatch, reverse
- from sentry.models.group import Group
- from sentry.search.events import constants
- from sentry.testutils.cases import APITestCase, MetricsEnhancedPerformanceTestCase, SnubaTestCase
- from sentry.testutils.helpers.datetime import before_now, iso_format
- from sentry.testutils.helpers.options import override_options
- from sentry.utils.samples import load_data
- from tests.sentry.issues.test_utils import OccurrenceTestMixin
- pytestmark = pytest.mark.sentry_metrics
- def format_project_event(project_id_or_slug, event_id):
- return f"{project_id_or_slug}:{event_id}"
- class OrganizationEventDetailsEndpointTest(APITestCase, SnubaTestCase, OccurrenceTestMixin):
- def setUp(self):
- super().setUp()
- min_ago = before_now(minutes=1).isoformat()
- two_min_ago = before_now(minutes=2).isoformat()
- three_min_ago = before_now(minutes=3).isoformat()
- self.login_as(user=self.user)
- self.project = self.create_project()
- self.project_2 = self.create_project()
- self.store_event(
- data={
- "event_id": "a" * 32,
- "message": "oh no",
- "timestamp": three_min_ago,
- "fingerprint": ["group-1"],
- },
- project_id=self.project.id,
- )
- self.store_event(
- data={
- "event_id": "b" * 32,
- "message": "very bad",
- "timestamp": two_min_ago,
- "fingerprint": ["group-1"],
- },
- project_id=self.project.id,
- )
- self.store_event(
- data={
- "event_id": "c" * 32,
- "message": "very bad",
- "timestamp": min_ago,
- "fingerprint": ["group-2"],
- },
- project_id=self.project.id,
- )
- self.groups = list(Group.objects.all().order_by("id"))
- def test_performance_flag(self):
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": "a" * 32,
- },
- )
- with self.feature(
- {"organizations:discover-basic": False, "organizations:performance-view": True}
- ):
- response = self.client.get(url, format="json")
- assert response.status_code == 200, response.content
- assert response.data["id"] == "a" * 32
- assert response.data["projectSlug"] == self.project.slug
- def test_simple(self):
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": "a" * 32,
- },
- )
- with self.feature("organizations:discover-basic"):
- response = self.client.get(url, format="json")
- assert response.status_code == 200, response.content
- assert response.data["id"] == "a" * 32
- assert response.data["projectSlug"] == self.project.slug
- @override_options({"api.id-or-slug-enabled": True})
- def test_simple_with_id(self):
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.id,
- "event_id": "a" * 32,
- },
- )
- with self.feature("organizations:discover-basic"):
- response = self.client.get(url, format="json")
- assert response.status_code == 200, response.content
- assert response.data["id"] == "a" * 32
- assert response.data["projectSlug"] == self.project.slug
- def test_simple_transaction(self):
- min_ago = before_now(minutes=1).isoformat()
- event = self.store_event(
- data={
- "event_id": "d" * 32,
- "type": "transaction",
- "transaction": "api.issue.delete",
- "spans": [],
- "contexts": {"trace": {"op": "foobar", "trace_id": "a" * 32, "span_id": "a" * 16}},
- "start_timestamp": before_now(minutes=1, seconds=5).isoformat(),
- "timestamp": min_ago,
- },
- project_id=self.project.id,
- )
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": event.event_id,
- },
- )
- with self.feature("organizations:discover-basic"):
- response = self.client.get(url, format="json")
- assert response.status_code == 200
- assert response.data["id"] == "d" * 32
- assert response.data["type"] == "transaction"
- def test_no_access_missing_feature(self):
- with self.feature({"organizations:discover-basic": False}):
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": "a" * 32,
- },
- )
- response = self.client.get(url, format="json")
- assert response.status_code == 404, response.content
- def test_access_non_member_project(self):
- # Add a new user to a project and then access events on project they are not part of.
- member_user = self.create_user()
- team = self.create_team(members=[member_user])
- self.create_project(organization=self.organization, teams=[team])
- # Enable open membership
- self.organization.flags.allow_joinleave = True
- self.organization.save()
- self.login_as(member_user)
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": "a" * 32,
- },
- )
- with self.feature("organizations:discover-basic"):
- response = self.client.get(url, format="json")
- assert response.status_code == 200, response.content
- # When open membership is off, access should be denied to non owner users
- self.organization.flags.allow_joinleave = False
- self.organization.save()
- with self.feature("organizations:discover-basic"):
- response = self.client.get(url, format="json")
- assert response.status_code == 404, response.content
- def test_no_event(self):
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": "d" * 32,
- },
- )
- with self.feature("organizations:discover-basic"):
- response = self.client.get(url, format="json")
- assert response.status_code == 404, response.content
- def test_invalid_event_id(self):
- with pytest.raises(NoReverseMatch):
- reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": "not-an-event",
- },
- )
- def test_long_trace_description(self):
- data = load_data("transaction")
- data["event_id"] = "d" * 32
- data["timestamp"] = before_now(minutes=1).isoformat()
- data["start_timestamp"] = iso_format(before_now(minutes=1) - timedelta(seconds=5))
- data["contexts"]["trace"]["description"] = "b" * 512
- self.store_event(data=data, project_id=self.project.id)
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": "d" * 32,
- },
- )
- with self.feature("organizations:discover-basic"):
- response = self.client.get(url, format="json")
- assert response.status_code == 200, response.content
- trace = response.data["contexts"]["trace"]
- original_trace = data["contexts"]["trace"]
- assert trace["trace_id"] == original_trace["trace_id"]
- assert trace["span_id"] == original_trace["span_id"]
- assert trace["parent_span_id"] == original_trace["parent_span_id"]
- assert trace["description"][:-3] in original_trace["description"]
- def test_blank_fields(self):
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": "a" * 32,
- },
- )
- with self.feature("organizations:discover-basic"):
- response = self.client.get(
- url,
- data={"field": ["", " "], "statsPeriod": "24h"},
- format="json",
- )
- assert response.status_code == 200, response.content
- assert response.data["id"] == "a" * 32
- assert response.data["projectSlug"] == self.project.slug
- def test_out_of_retention(self):
- self.store_event(
- data={
- "event_id": "d" * 32,
- "message": "oh no",
- "timestamp": before_now(days=2).isoformat(),
- "fingerprint": ["group-1"],
- },
- project_id=self.project.id,
- )
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": "d" * 32,
- },
- )
- with self.options({"system.event-retention-days": 1}):
- response = self.client.get(
- url,
- format="json",
- )
- assert response.status_code == 404, response.content
- def test_generic_event(self):
- occurrence, _ = self.process_occurrence(
- project_id=self.project.id,
- event_data={
- "level": "info",
- },
- )
- url = reverse(
- "sentry-api-0-organization-event-details",
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": occurrence.event_id,
- },
- )
- with self.feature("organizations:discover-basic"):
- response = self.client.get(url, format="json")
- assert response.status_code == 200, response.content
- assert response.data["id"] == occurrence.event_id
- assert response.data["projectSlug"] == self.project.slug
- assert response.data["occurrence"] is not None
- assert response.data["occurrence"]["id"] == occurrence.id
- class EventComparisonTest(MetricsEnhancedPerformanceTestCase):
- endpoint = "sentry-api-0-organization-event-details"
- def setUp(self):
- self.init_snuba()
- self.ten_mins_ago = before_now(minutes=10)
- self.transaction_data = load_data("transaction", timestamp=self.ten_mins_ago)
- self.RESULT_COLUMN = "span.averageResults"
- event = self.store_event(self.transaction_data, self.project)
- self.url = reverse(
- self.endpoint,
- kwargs={
- "organization_id_or_slug": self.project.organization.slug,
- "project_id_or_slug": self.project.slug,
- "event_id": event.event_id,
- },
- )
- self.login_as(user=self.user)
- self.store_span_metric(
- 1,
- internal_metric=constants.SELF_TIME_LIGHT,
- timestamp=self.ten_mins_ago,
- tags={"span.group": "26b881987e4bad99"},
- )
- def test_get_without_feature(self):
- response = self.client.get(self.url, {"averageColumn": "span.self_time"})
- assert response.status_code == 200, response.content
- entries = response.data["entries"] # type: ignore[attr-defined]
- for entry in entries:
- if entry["type"] == "spans":
- for span in entry["data"]:
- assert span.get(self.RESULT_COLUMN) is None
- def test_get(self):
- with self.feature("organizations:insights-initial-modules"):
- response = self.client.get(self.url, {"averageColumn": "span.self_time"})
- assert response.status_code == 200, response.content
- entries = response.data["entries"] # type: ignore[attr-defined]
- for entry in entries:
- if entry["type"] == "spans":
- for span in entry["data"]:
- if span["op"] == "db":
- assert span[self.RESULT_COLUMN] == {"avg(span.self_time)": 1.0}
- if span["op"] == "django.middleware":
- assert self.RESULT_COLUMN not in span
- def test_get_multiple_columns(self):
- self.store_span_metric(
- 2,
- internal_metric=constants.SPAN_METRICS_MAP["span.duration"],
- timestamp=self.ten_mins_ago,
- tags={"span.group": "26b881987e4bad99"},
- )
- with self.feature("organizations:insights-initial-modules"):
- response = self.client.get(
- self.url, {"averageColumn": ["span.self_time", "span.duration"]}
- )
- assert response.status_code == 200, response.content
- entries = response.data["entries"] # type: ignore[attr-defined]
- for entry in entries:
- if entry["type"] == "spans":
- for span in entry["data"]:
- if span["op"] == "db":
- assert span[self.RESULT_COLUMN] == {
- "avg(span.self_time)": 1.0,
- "avg(span.duration)": 2.0,
- }
- if span["op"] == "django.middlewares":
- assert self.RESULT_COLUMN not in span
- def test_nan_column(self):
- # If there's nothing stored for a metric, span.duration in this case the query returns nan
- with self.feature("organizations:insights-initial-modules"):
- response = self.client.get(
- self.url, {"averageColumn": ["span.self_time", "span.duration"]}
- )
- assert response.status_code == 200, response.content
- entries = response.data["entries"] # type: ignore[attr-defined]
- for entry in entries:
- if entry["type"] == "spans":
- for span in entry["data"]:
- if span["op"] == "db":
- assert span[self.RESULT_COLUMN] == {"avg(span.self_time)": 1.0}
- if span["op"] == "django.middlewares":
- assert self.RESULT_COLUMN not in span
- def test_invalid_column(self):
- # If any columns are invalid, ignore average field in results completely
- response = self.client.get(
- self.url, {"averageColumn": ["span.self_time", "span.everything"]}
- )
- assert response.status_code == 200, response.content
- entries = response.data["entries"] # type: ignore[attr-defined]
- for entry in entries:
- if entry["type"] == "spans":
- for span in entry["data"]:
- assert self.RESULT_COLUMN not in span
|