Skip to content

Commit 6982620

Browse files
fix(orgstats): Update the query to be able to filter by user's project (#102762)
closes https://linear.app/getsentry/issue/TET-1370/check-filters-on-stats-and-usage-page-for-project-dropdown
1 parent 3262877 commit 6982620

File tree

4 files changed

+158
-16
lines changed

4 files changed

+158
-16
lines changed

src/sentry/api/endpoints/organization_stats_v2.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def build_outcomes_query(self, request: Request, organization):
216216
return QueryDefinition.from_query_dict(request.GET, params)
217217

218218
def _get_projects_for_orgstats_query(self, request: Request, organization):
219-
# look at the raw project_id filter passed in, if its empty
219+
# look at the raw project_id filter passed in, if its -1
220220
# and project_id is not in groupBy filter, treat it as an
221221
# org wide query and don't pass project_id in to QueryDefinition
222222
req_proj_ids = self.get_requested_project_ids_unchecked(request)
@@ -229,11 +229,10 @@ def _get_projects_for_orgstats_query(self, request: Request, organization):
229229
return [p.id for p in projects]
230230

231231
def _is_org_total_query(self, request: Request, project_ids):
232-
return all(
233-
[
234-
not project_ids or project_ids == ALL_ACCESS_PROJECTS,
235-
"project" not in request.GET.get("groupBy", []),
236-
]
232+
# ALL_ACCESS_PROJECTS ({-1}) signals that stats should aggregate across
233+
# all projects rather than filtering to specific project IDs
234+
return project_ids == ALL_ACCESS_PROJECTS and "project" not in request.GET.get(
235+
"groupBy", []
237236
)
238237

239238
@contextmanager

static/app/views/organizationStats/index.spec.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,15 @@ describe('OrganizationStats', () => {
7575
* Base + Error Handling
7676
*/
7777
it('renders the base view', async () => {
78-
render(<OrganizationStats />, {organization});
78+
render(<OrganizationStats />, {
79+
organization,
80+
initialRouterConfig: {
81+
location: {
82+
pathname: '/organizations/org-slug/stats/',
83+
query: {project: [ALL_ACCESS_PROJECTS.toString()]},
84+
},
85+
},
86+
});
7987

8088
expect(await screen.findByTestId('usage-stats-chart')).toBeInTheDocument();
8189

@@ -245,6 +253,12 @@ describe('OrganizationStats', () => {
245253
OrganizationStore.onUpdate(newOrg, {replace: true});
246254
render(<OrganizationStats />, {
247255
organization: newOrg,
256+
initialRouterConfig: {
257+
location: {
258+
pathname: '/organizations/org-slug/stats/',
259+
query: {project: [ALL_ACCESS_PROJECTS.toString()]},
260+
},
261+
},
248262
});
249263

250264
expect(await screen.findByText('All Projects')).toBeInTheDocument();

static/app/views/organizationStats/index.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilte
1919
import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
2020
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
2121
import {DATA_CATEGORY_INFO, DEFAULT_STATS_PERIOD} from 'sentry/constants';
22-
import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
2322
import {t, tct} from 'sentry/locale';
2423
import ConfigStore from 'sentry/stores/configStore';
2524
import {space} from 'sentry/styles/space';
@@ -167,10 +166,7 @@ export class OrganizationStatsInner extends Component<OrganizationStatsProps> {
167166

168167
// Project selection from GlobalSelectionHeader
169168
get projectIds(): number[] {
170-
const selection_projects = this.props.selection.projects.length
171-
? this.props.selection.projects
172-
: [ALL_ACCESS_PROJECTS];
173-
return selection_projects;
169+
return this.props.selection.projects;
174170
}
175171

176172
get isSingleProject(): boolean {

tests/snuba/api/endpoints/test_organization_stats_v2.py

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,17 @@ def setUp(self) -> None:
3333
self.project3 = self.create_project(organization=self.org2)
3434

3535
self.user2 = self.create_user(is_superuser=False)
36+
self.user3 = self.create_user(is_superuser=False)
3637
self.create_member(user=self.user2, organization=self.organization, role="member", teams=[])
3738
self.create_member(user=self.user2, organization=self.org3, role="member", teams=[])
3839
self.project4 = self.create_project(
3940
name="users2sproj",
4041
teams=[self.create_team(organization=self.org, members=[self.user2])],
4142
)
43+
self.project5 = self.create_project(
44+
name="users3sproj",
45+
teams=[self.create_team(organization=self.org, members=[self.user3])],
46+
)
4247

4348
self.store_outcomes(
4449
{
@@ -85,6 +90,18 @@ def setUp(self) -> None:
8590
"quantity": 1,
8691
}
8792
)
93+
self.store_outcomes(
94+
{
95+
"org_id": self.org.id,
96+
"timestamp": self._now - timedelta(hours=1),
97+
"project_id": self.project5.id,
98+
"outcome": Outcome.ACCEPTED,
99+
"reason": "none",
100+
"category": DataCategory.ERROR,
101+
"quantity": 1,
102+
},
103+
2,
104+
)
88105

89106
# Add profile_duration outcome data
90107
self.store_outcomes(
@@ -284,7 +301,7 @@ def test_timeseries_interval(self) -> None:
284301
isoformat_z(floor_to_utc_day(self._now)),
285302
],
286303
"groups": [
287-
{"by": {}, "series": {"sum(quantity)": [0, 6]}, "totals": {"sum(quantity)": 6}}
304+
{"by": {}, "series": {"sum(quantity)": [0, 8]}, "totals": {"sum(quantity)": 8}}
288305
],
289306
"start": isoformat_z(floor_to_utc_day(self._now) - timedelta(days=1)),
290307
"end": isoformat_z(floor_to_utc_day(self._now) + timedelta(days=1)),
@@ -312,8 +329,8 @@ def test_timeseries_interval(self) -> None:
312329
"groups": [
313330
{
314331
"by": {},
315-
"series": {"sum(quantity)": [0, 0, 0, 6, 0]},
316-
"totals": {"sum(quantity)": 6},
332+
"series": {"sum(quantity)": [0, 0, 0, 8, 0]},
333+
"totals": {"sum(quantity)": 8},
317334
}
318335
],
319336
"start": isoformat_z(
@@ -344,7 +361,7 @@ def test_user_org_total_all_accessible(self) -> None:
344361
isoformat_z(floor_to_utc_day(self._now)),
345362
],
346363
"groups": [
347-
{"by": {}, "series": {"sum(quantity)": [0, 7]}, "totals": {"sum(quantity)": 7}}
364+
{"by": {}, "series": {"sum(quantity)": [0, 9]}, "totals": {"sum(quantity)": 9}}
348365
],
349366
}
350367

@@ -450,6 +467,10 @@ def test_open_membership_semantics(self) -> None:
450467
"by": {"project": self.project2.id},
451468
"totals": {"sum(quantity)": 1},
452469
},
470+
{
471+
"by": {"project": self.project5.id},
472+
"totals": {"sum(quantity)": 2},
473+
},
453474
],
454475
}
455476

@@ -973,6 +994,118 @@ def test_profile_duration_groupby(self) -> None:
973994
],
974995
}
975996

997+
@freeze_time(_now)
998+
def test_project_filtering_with_all_projects(self) -> None:
999+
"""Test that project=-1 aggregates data across all projects in the org"""
1000+
response = self.do_request(
1001+
{
1002+
"project": [-1],
1003+
"statsPeriod": "1d",
1004+
"interval": "1d",
1005+
"field": ["sum(quantity)"],
1006+
"category": ["error", "transaction"],
1007+
},
1008+
status_code=200,
1009+
)
1010+
1011+
assert response.data["groups"] == [
1012+
{
1013+
"by": {},
1014+
"totals": {"sum(quantity)": 9},
1015+
"series": {"sum(quantity)": [0, 9]},
1016+
}
1017+
]
1018+
1019+
@freeze_time(_now)
1020+
def test_project_filtering_without_project_param(self) -> None:
1021+
"""Test that when no project parameter is provided, it filters by user's projects (my projects)"""
1022+
response = self.do_request(
1023+
{
1024+
"statsPeriod": "1d",
1025+
"interval": "1d",
1026+
"field": ["sum(quantity)"],
1027+
"category": ["error", "transaction"],
1028+
},
1029+
status_code=200,
1030+
)
1031+
1032+
assert response.data["groups"] == [
1033+
{
1034+
"by": {},
1035+
"totals": {"sum(quantity)": 7},
1036+
"series": {"sum(quantity)": [0, 7]},
1037+
}
1038+
]
1039+
1040+
@freeze_time(_now)
1041+
def test_project_filtering_with_specific_project(self) -> None:
1042+
"""Test that when a specific project id is provided, it filters by that project only"""
1043+
response = self.do_request(
1044+
{
1045+
"project": [self.project.id],
1046+
"statsPeriod": "1d",
1047+
"interval": "1d",
1048+
"field": ["sum(quantity)"],
1049+
"category": ["error", "transaction"],
1050+
},
1051+
status_code=200,
1052+
)
1053+
1054+
assert response.data["groups"] == [
1055+
{
1056+
"by": {},
1057+
"totals": {"sum(quantity)": 6},
1058+
"series": {"sum(quantity)": [0, 6]},
1059+
}
1060+
]
1061+
1062+
@freeze_time(_now)
1063+
def test_project_filtering_with_multiple_specific_projects(self) -> None:
1064+
"""Test filtering with multiple specific project IDs"""
1065+
response = self.do_request(
1066+
{
1067+
"project": [self.project.id, self.project2.id],
1068+
"statsPeriod": "1d",
1069+
"interval": "1d",
1070+
"field": ["sum(quantity)"],
1071+
"category": ["error", "transaction"],
1072+
},
1073+
status_code=200,
1074+
)
1075+
1076+
assert response.data["groups"] == [
1077+
{
1078+
"by": {},
1079+
"totals": {"sum(quantity)": 7},
1080+
"series": {"sum(quantity)": [0, 7]},
1081+
}
1082+
]
1083+
1084+
@freeze_time(_now)
1085+
def test_with_groupby_project(self) -> None:
1086+
"""Test that groupBy=project shows individual project stats"""
1087+
response = self.do_request(
1088+
{
1089+
"statsPeriod": "1d",
1090+
"interval": "1d",
1091+
"field": ["sum(quantity)"],
1092+
"category": ["error", "transaction"],
1093+
"groupBy": ["project"],
1094+
},
1095+
status_code=200,
1096+
)
1097+
1098+
assert response.data["groups"] == [
1099+
{
1100+
"by": {"project": self.project.id},
1101+
"totals": {"sum(quantity)": 6},
1102+
},
1103+
{
1104+
"by": {"project": self.project2.id},
1105+
"totals": {"sum(quantity)": 1},
1106+
},
1107+
]
1108+
9761109

9771110
def result_sorted(result):
9781111
"""sort the groups of the results array by the `by` object, ensuring a stable order"""

0 commit comments

Comments
 (0)