Repository: getsentry/sentry

Issue:
Dashboard showing data for all environments, no matter which one selected
### Environment

SaaS (https://sentry.io/)

### Steps to Reproduce

1. Go to customer dashboard (see linked Jira ticket for URL)
2. Look at "User Builds" widget
3. Note values of CRASH FREE RATE, UNIQUE USERS and TOTAL SESSIONS for release `4.3.7 (2)`
4. Change environment filter from "Production" to "All Environments"
5. Observe exact same values being displayed. <-- **THIS IS INCORRECT**
6. Click on `4.3.7 (2)` release which takes you to /releases page
7. Change time range to match Dashboard time range (14d)
8. Observe that THIS RELEASE column shows the same values as Dashboard does in Step [3] and [5].
9. Change environment filter from "All Environments" to "Production" 
10. Observe different values (what should've been shown in Step [5])

### Expected Result

in [5] we should see same values as in [10]

### Actual Result

instead in [5] we see value based on data from ALL environments, which is same as [3] and is incorrect.

### Product Area

Dashboards

### Link

_No response_

### DSN

_No response_

### Version

_No response_

┆Issue is synchronized with this [Jira Improvement](https://getsentry.atlassian.net/browse/FEEDBACK-1915) by [Unito](https://www.unito.io)



Discussion:
Assigning to @getsentry/support for [routing](https://open.sentry.io/triage/#2-route) ⏲️
Routing to @getsentry/product-owners-dashboards for [triage](https://develop.sentry.dev/processing-tickets/#3-triage) ⏲️
Yes, this appears to be a bug. Looking into it to diagnose further.

Resolving patch:
diff --git a/src/sentry/snuba/metrics/query_builder.py b/src/sentry/snuba/metrics/query_builder.py
--- a/src/sentry/snuba/metrics/query_builder.py
+++ b/src/sentry/snuba/metrics/query_builder.py
@@ -4,7 +4,7 @@
     "SnubaResultConverter",
     "get_date_range",
     "parse_field",
-    "parse_query",
+    "parse_conditions",
     "resolve_tags",
     "translate_meta_results",
     "QUERY_PROJECT_LIMIT",
@@ -89,6 +89,27 @@
 QUERY_PROJECT_LIMIT = 10
 
 
+def _strip_project_id(condition: Condition) -> Optional[Condition]:
+    if isinstance(condition, BooleanCondition):
+        new_boolean_condition = BooleanCondition()
+        for nested_condition in condition.conditions:
+            stripped_condition = _strip_project_id(nested_condition)
+            # Only if we have a condition object we append, since if its None it means that we directly had a
+            # project.id.
+            if stripped_condition is not None:
+                new_boolean_condition.conditions.append(stripped_condition)
+
+        return new_boolean_condition if new_boolean_condition.conditions else None
+    elif isinstance(condition, Condition):
+        if isinstance(condition.lhs, Column):
+            if condition.lhs.name == "project_id":
+                return None
+
+            return condition
+
+    return None
+
+
 def parse_field(field: str, allow_mri: bool = False, allow_private: bool = False) -> MetricField:
 
     if allow_mri:
@@ -406,24 +427,28 @@ def is_tag_key_allowed(tag_key: str, allowed_tag_keys: Optional[Dict[str, str]])
     return allowed_tag_keys.get(f"tags[{tag_key}]") is not None
 
 
-def parse_query(query_string: str, projects: Sequence[Project]) -> Sequence[Condition]:
-    """Parse given filter query into a list of snuba conditions"""
+def parse_conditions(
+    query_string: str, projects: Sequence[Project], environments: Sequence[str]
+) -> Sequence[Condition]:
+    """Parse given filter query and query params into a list of snuba conditions"""
     # HACK: Parse a sessions query, validate / transform afterwards.
     # We will want to write our own grammar + interpreter for this later.
     try:
         query_builder = ReleaseHealthQueryBuilder(
             Dataset.Sessions,
             params={
+                "environment": environments,
                 "project_id": [project.id for project in projects],
                 "organization_id": org_id_from_projects(projects) if projects else None,
             },
             config=QueryBuilderConfig(use_aggregate_conditions=True),
         )
         where, _ = query_builder.resolve_conditions(query_string)
+        param_conditions = list(filter(_strip_project_id, query_builder.resolve_params()))
     except InvalidSearchQuery as e:
-        raise InvalidParams(f"Failed to parse query: {e}")
+        raise InvalidParams(f"Failed to parse conditions: {e}")
 
-    return where
+    return where + param_conditions
 
 
 class ReleaseHealthQueryBuilder(UnresolvedQuery):
@@ -469,7 +494,6 @@ def __init__(
         paginator_kwargs = paginator_kwargs or {}
 
         self.query = query_params.get("query", "")
-        self.parsed_query = parse_query(self.query, projects) if self.query else None
         self.groupby = [
             MetricGroupByField(groupby_col) for groupby_col in query_params.getlist("groupBy", [])
         ]
@@ -485,6 +509,9 @@ def __init__(
         self.limit: Optional[Limit] = self._parse_limit(paginator_kwargs)
         self.offset: Optional[Offset] = self._parse_offset(paginator_kwargs)
         self.having: Optional[ConditionGroup] = query_params.getlist("having")
+        self.where = parse_conditions(
+            self.query, projects, environments=query_params.getlist("environment")
+        )
 
         start, end, rollup = get_date_range(query_params)
         self.rollup = rollup
@@ -502,7 +529,7 @@ def to_metrics_query(self) -> MetricsQuery:
             select=self.fields,
             start=self.start,
             end=self.end,
-            where=self.parsed_query,
+            where=self.where,
             having=self.having,
             groupby=self.groupby,
             orderby=self.orderby,


Test patch:
diff --git a/tests/sentry/api/endpoints/test_organization_metric_data.py b/tests/sentry/api/endpoints/test_organization_metric_data.py
--- a/tests/sentry/api/endpoints/test_organization_metric_data.py
+++ b/tests/sentry/api/endpoints/test_organization_metric_data.py
@@ -466,7 +466,7 @@ def test_query_with_wildcard(self):
         assert response.status_code == 400
         assert (
             response.data["detail"]
-            == "Failed to parse query: Release Health Queries don't support wildcards"
+            == "Failed to parse conditions: Release Health Queries don't support wildcards"
         )
 
     def test_pagination_offset_without_orderby(self):
diff --git a/tests/sentry/snuba/metrics/test_query.py b/tests/sentry/snuba/metrics/test_query.py
--- a/tests/sentry/snuba/metrics/test_query.py
+++ b/tests/sentry/snuba/metrics/test_query.py
@@ -19,7 +19,7 @@
     MetricGroupByField,
     MetricOrderByField,
     MetricsQuery,
-    parse_query,
+    parse_conditions,
 )
 from sentry.snuba.metrics.naming_layer import SessionMRI, TransactionMRI
 from sentry.testutils.pytest.fixtures import django_db_all
@@ -459,7 +459,7 @@ def test_validate_distribution_functions_in_orderby():
 @django_db_all
 def test_validate_where():
     query = "session.status:crashed"
-    where = parse_query(query, [])
+    where = parse_conditions(query, [], [])
 
     with pytest.raises(InvalidParams, match="Tag name session.status is not a valid query filter"):
         MetricsQuery(**MetricsQueryBuilder().with_where(where).to_metrics_query_dict())
diff --git a/tests/sentry/snuba/metrics/test_query_builder.py b/tests/sentry/snuba/metrics/test_query_builder.py
--- a/tests/sentry/snuba/metrics/test_query_builder.py
+++ b/tests/sentry/snuba/metrics/test_query_builder.py
@@ -48,7 +48,7 @@
     SnubaResultConverter,
     get_date_range,
     get_intervals,
-    parse_query,
+    parse_conditions,
     resolve_tags,
     translate_meta_results,
 )
@@ -249,7 +249,7 @@ def get_entity_of_metric_mocked(_, metric_name, use_case_id):
         ),
     ],
 )
-def test_parse_query(query_string, expected):
+def test_parse_conditions(query_string, expected):
     org_id = ORG_ID
     use_case_id = UseCaseID.SESSIONS
     for s in ("myapp@2.0.0", "/bar/:orgId/"):
@@ -258,7 +258,7 @@ def test_parse_query(query_string, expected):
     parsed = resolve_tags(
         use_case_id,
         org_id,
-        parse_query(query_string, []),
+        parse_conditions(query_string, [], []),
         [],
     )
     assert parsed == expected()
@@ -478,6 +478,7 @@ def expected_query(match, select, extra_groupby, metric_name):
     }
 
 
+@django_db_all
 @mock.patch("sentry.snuba.sessions_v2.get_now", return_value=MOCK_NOW)
 @mock.patch("sentry.api.utils.timezone.now", return_value=MOCK_NOW)
 @mock.patch(
@@ -560,6 +561,7 @@ def test_build_snuba_query_mri(mock_now, mock_now2):
         )
 
 
+@django_db_all
 @mock.patch("sentry.snuba.sessions_v2.get_now", return_value=MOCK_NOW)
 @mock.patch("sentry.api.utils.timezone.now", return_value=MOCK_NOW)
 @mock.patch(
@@ -946,6 +948,7 @@ def test_build_snuba_query_with_derived_alias(mock_now, mock_now2):
     )
 
 
+@django_db_all
 @mock.patch("sentry.snuba.sessions_v2.get_now", return_value=MOCK_NOW)
 @mock.patch("sentry.api.utils.timezone.now", return_value=MOCK_NOW)
 def test_translate_results_derived_metrics(_1, _2):
@@ -1067,6 +1070,7 @@ def test_translate_results_derived_metrics(_1, _2):
     ]
 
 
+@django_db_all
 @mock.patch("sentry.snuba.sessions_v2.get_now", return_value=MOCK_NOW)
 @mock.patch("sentry.api.utils.timezone.now", return_value=MOCK_NOW)
 def test_translate_results_missing_slots(_1, _2):
@@ -1505,7 +1509,7 @@ def test_valid_latest_release_alias_filter(self):
             }
         )
         query = QueryDefinition([self.project], query_params)
-        assert query.parsed_query == [
+        assert query.where == [
             Condition(
                 Column(name="release"),
                 Op.IN,
@@ -1513,6 +1517,49 @@ def test_valid_latest_release_alias_filter(self):
             )
         ]
 
+    def test_single_environment_is_passed_through_to_metrics_query(self):
+        self.create_environment(name="alpha", project=self.project)
+        query_params = MultiValueDict(
+            {
+                "environment": ["alpha"],
+                "query": [""],
+                "field": [
+                    "sum(sentry.sessions.session)",
+                ],
+            }
+        )
+        query = QueryDefinition([self.project], query_params)
+        actual_result = query.to_metrics_query()
+        assert actual_result.where == [
+            Condition(
+                Column(name="environment"),
+                Op.EQ,
+                rhs="alpha",
+            )
+        ]
+
+    def test_multiple_environments_are_passed_through_to_metrics_query(self):
+        self.create_environment(name="alpha", project=self.project)
+        self.create_environment(name="beta", project=self.project)
+        query_params = MultiValueDict(
+            {
+                "environment": ["alpha", "beta"],
+                "query": [""],
+                "field": [
+                    "sum(sentry.sessions.session)",
+                ],
+            }
+        )
+        query = QueryDefinition([self.project], query_params)
+        actual_result = query.to_metrics_query()
+        assert actual_result.where == [
+            Condition(
+                Column(name="environment"),
+                Op.IN,
+                rhs=["alpha", "beta"],
+            )
+        ]
+
 
 class ResolveTagsTestCase(TestCase):
     def setUp(self):