|
1 | 1 | import uuid |
2 | | -from datetime import datetime, timedelta |
| 2 | +from datetime import UTC, datetime, timedelta |
3 | 3 | from typing import Literal |
4 | 4 | from unittest.mock import patch |
5 | 5 |
|
6 | 6 | import pytest |
7 | 7 | from pydantic import BaseModel |
8 | 8 |
|
| 9 | +from sentry.api import client |
9 | 10 | from sentry.constants import ObjectStatus |
10 | 11 | from sentry.models.group import Group |
11 | 12 | from sentry.models.groupassignee import GroupAssignee |
12 | 13 | from sentry.models.repository import Repository |
13 | 14 | from sentry.seer.endpoints.seer_rpc import get_organization_project_ids |
14 | 15 | from sentry.seer.explorer.tools import ( |
| 16 | + EVENT_TIMESERIES_RESOLUTIONS, |
15 | 17 | execute_trace_query_chart, |
16 | 18 | execute_trace_query_table, |
17 | 19 | get_issue_details, |
|
21 | 23 | from sentry.seer.sentry_data_models import EAPTrace |
22 | 24 | from sentry.testutils.cases import APITransactionTestCase, SnubaTestCase, SpanTestCase |
23 | 25 | from sentry.testutils.helpers.datetime import before_now |
| 26 | +from sentry.utils.dates import parse_stats_period |
24 | 27 | from sentry.utils.samples import load_data |
25 | 28 | from tests.sentry.issues.test_utils import OccurrenceTestMixin |
26 | 29 |
|
@@ -631,6 +634,20 @@ class _SentryEventData(BaseModel): |
631 | 634 |
|
632 | 635 | class TestGetIssueDetails(APITransactionTestCase, SnubaTestCase, OccurrenceTestMixin): |
633 | 636 |
|
| 637 | + def _validate_event_timeseries(self, timeseries: dict): |
| 638 | + assert isinstance(timeseries, dict) |
| 639 | + assert "count()" in timeseries |
| 640 | + assert "data" in timeseries["count()"] |
| 641 | + assert isinstance(timeseries["count()"]["data"], list) |
| 642 | + for item in timeseries["count()"]["data"]: |
| 643 | + assert len(item) == 2 |
| 644 | + assert isinstance(item[0], int) |
| 645 | + assert isinstance(item[1], list) |
| 646 | + assert len(item[1]) == 1 |
| 647 | + assert isinstance(item[1][0], dict) |
| 648 | + assert "count" in item[1][0] |
| 649 | + assert isinstance(item[1][0]["count"], int) |
| 650 | + |
634 | 651 | @patch("sentry.models.group.get_recommended_event") |
635 | 652 | @patch("sentry.seer.explorer.tools.get_all_tags_overview") |
636 | 653 | def _test_get_issue_details_success( |
@@ -717,6 +734,9 @@ def _test_get_issue_details_success( |
717 | 734 | else: |
718 | 735 | assert result["event_trace_id"] is None |
719 | 736 |
|
| 737 | + # Validate timeseries dict structure. |
| 738 | + self._validate_event_timeseries(result["event_timeseries"]) |
| 739 | + |
720 | 740 | def test_get_issue_details_success_int_id(self): |
721 | 741 | self._test_get_issue_details_success(use_short_id=False) |
722 | 742 |
|
@@ -861,6 +881,64 @@ def test_get_issue_details_with_assigned_team(self, mock_get_tags, mock_get_reco |
861 | 881 | assert md.assignedTo.name == self.team.slug |
862 | 882 | assert md.assignedTo.email is None |
863 | 883 |
|
| 884 | + @patch("sentry.seer.explorer.tools.client") |
| 885 | + @patch("sentry.models.group.get_recommended_event") |
| 886 | + @patch("sentry.seer.explorer.tools.get_all_tags_overview") |
| 887 | + def test_get_issue_details_timeseries_resolution( |
| 888 | + self, |
| 889 | + mock_get_tags, |
| 890 | + mock_get_recommended_event, |
| 891 | + mock_api_client, |
| 892 | + ): |
| 893 | + """Test groups with different first_seen dates""" |
| 894 | + mock_get_tags.return_value = {"tags_overview": [{"key": "test_tag", "top_values": []}]} |
| 895 | + # Passthrough to real client - allows testing call args |
| 896 | + mock_api_client.get.side_effect = client.get |
| 897 | + |
| 898 | + for stats_period, interval in EVENT_TIMESERIES_RESOLUTIONS: |
| 899 | + delta = parse_stats_period(stats_period) |
| 900 | + assert delta is not None |
| 901 | + if delta > timedelta(days=30): |
| 902 | + # Skip the 90d test as the retention for testutils is 30d. |
| 903 | + continue |
| 904 | + |
| 905 | + # Set a first_seen date slightly newer than the stats period we're testing. |
| 906 | + first_seen = datetime.now(UTC) - delta + timedelta(minutes=6, seconds=7) |
| 907 | + data = load_data("python", timestamp=first_seen) |
| 908 | + data["exception"] = {"values": [{"type": "Exception", "value": "Test exception"}]} |
| 909 | + event = self.store_event(data=data, project_id=self.project.id) |
| 910 | + mock_get_recommended_event.return_value = event |
| 911 | + |
| 912 | + # Second newer event |
| 913 | + data = load_data("python", timestamp=first_seen + timedelta(minutes=6, seconds=7)) |
| 914 | + data["exception"] = {"values": [{"type": "Exception", "value": "Test exception"}]} |
| 915 | + self.store_event(data=data, project_id=self.project.id) |
| 916 | + |
| 917 | + group = event.group |
| 918 | + assert isinstance(group, Group) |
| 919 | + assert group.first_seen == first_seen |
| 920 | + |
| 921 | + result = get_issue_details( |
| 922 | + issue_id=str(group.id), |
| 923 | + organization_id=self.organization.id, |
| 924 | + selected_event="recommended", |
| 925 | + ) |
| 926 | + |
| 927 | + # Assert expected stats params were passed to the API. |
| 928 | + _, kwargs = mock_api_client.get.call_args |
| 929 | + assert kwargs["path"] == f"/organizations/{self.organization.slug}/events-stats/" |
| 930 | + assert kwargs["params"]["statsPeriod"] == stats_period |
| 931 | + assert kwargs["params"]["interval"] == interval |
| 932 | + |
| 933 | + # Validate final results. |
| 934 | + assert result is not None |
| 935 | + self._validate_event_timeseries(result["event_timeseries"]) |
| 936 | + assert result["timeseries_stats_period"] == stats_period |
| 937 | + assert result["timeseries_interval"] == interval |
| 938 | + |
| 939 | + # Ensure next iteration makes a fresh group. |
| 940 | + group.delete() |
| 941 | + |
864 | 942 |
|
865 | 943 | @pytest.mark.django_db(databases=["default", "control"]) |
866 | 944 | class TestGetRepositoryDefinition(APITransactionTestCase): |
|
0 commit comments