Skip to content

Commit 515d2a1

Browse files
impr(jira): switch to paginated project endpoint for jira issue config (#89707)
1 parent 9ccc5d2 commit 515d2a1

File tree

5 files changed

+176
-13
lines changed

5 files changed

+176
-13
lines changed

Diff for: src/sentry/integrations/jira/client.py

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class JiraCloudClient(ApiClient):
2929
ISSUE_URL = "/rest/api/2/issue/%s"
3030
META_URL = "/rest/api/2/issue/createmeta"
3131
PRIORITIES_URL = "/rest/api/2/priority"
32+
PROJECTS_PAGINATED_URL = "/rest/api/2/project/search"
3233
PROJECT_URL = "/rest/api/2/project"
3334
SEARCH_URL = "/rest/api/2/search/"
3435
VERSIONS_URL = "/rest/api/2/project/%s/versions"
@@ -122,7 +123,12 @@ def create_comment(self, issue_key, comment):
122123
def update_comment(self, issue_key, comment_id, comment):
123124
return self.put(self.COMMENT_URL % (issue_key, comment_id), data={"body": comment})
124125

126+
def get_projects_paginated(self, params: dict[str, Any] | None = None):
127+
response = self.get(self.PROJECTS_PAGINATED_URL, params=params)
128+
return response
129+
125130
def get_projects_list(self):
131+
"""deprecated - please use paginated projects endpoint"""
126132
return self.get_cached(self.PROJECT_URL)
127133

128134
def get_project_key_for_id(self, project_id) -> str:

Diff for: src/sentry/integrations/jira/endpoints/search.py

+17
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from rest_framework.request import Request
66
from rest_framework.response import Response
77

8+
from sentry import features
89
from sentry.api.api_owners import ApiOwner
910
from sentry.api.api_publish_status import ApiPublishStatus
1011
from sentry.api.base import control_silo_endpoint
1112
from sentry.integrations.api.bases.integration import IntegrationEndpoint
13+
from sentry.integrations.jira.integration import JiraProjectMapping
1214
from sentry.integrations.models.integration import Integration
1315
from sentry.organizations.services.organization import RpcOrganization
1416
from sentry.shared_integrations.exceptions import ApiError, ApiUnauthorized, IntegrationError
@@ -84,6 +86,21 @@ def get(
8486
users = [{"value": user_id, "label": display} for user_id, display in user_tuples]
8587
return Response(users)
8688

89+
if field == "project" and features.has(
90+
"organizations:jira-paginated-projects", organization, actor=request.user
91+
):
92+
try:
93+
response = jira_client.get_projects_paginated(params={"query": query})
94+
except (ApiUnauthorized, ApiError):
95+
return Response({"detail": "Unable to fetch projects from Jira"}, status=400)
96+
97+
projects = [
98+
JiraProjectMapping(label=f"{p["key"]} - {p["name"]}", value=p["id"])
99+
for p in response.get("values", [])
100+
]
101+
102+
return Response(projects)
103+
87104
try:
88105
response = jira_client.get_field_autocomplete(name=field, value=query)
89106
except (ApiUnauthorized, ApiError):

Diff for: src/sentry/integrations/jira/integration.py

+23-9
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,13 @@ def get_create_issue_config(self, group: Group | None, user: RpcUser, **kwargs):
789789
project_id = params.get("project", defaults.get("project"))
790790
client = self.get_client()
791791
try:
792-
jira_projects = client.get_projects_list()
792+
jira_projects = (
793+
client.get_projects_paginated({"maxResults": MAX_PER_PROJECT_QUERIES})["values"]
794+
if features.has(
795+
"organizations:jira-paginated-projects", group.organization, actor=user
796+
)
797+
else client.get_projects_list()
798+
)
793799
except ApiError as e:
794800
logger.info(
795801
"jira.get-create-issue-config.no-projects",
@@ -822,15 +828,23 @@ def get_create_issue_config(self, group: Group | None, user: RpcUser, **kwargs):
822828
if not any(c for c in issue_type_choices if c[0] == issue_type):
823829
issue_type = issue_type_meta["id"]
824830

831+
projects_form_field = {
832+
"name": "project",
833+
"label": "Jira Project",
834+
"choices": [(p["id"], f"{p["key"]} - {p["name"]}") for p in jira_projects],
835+
"default": meta["id"],
836+
"type": "select",
837+
"updatesForm": True,
838+
"required": True,
839+
}
840+
if features.has("organizations:jira-paginated-projects", group.organization, actor=user):
841+
paginated_projects_url = reverse(
842+
"sentry-extensions-jira-search", args=[self.organization.slug, self.model.id]
843+
)
844+
projects_form_field["url"] = paginated_projects_url
845+
825846
fields = [
826-
{
827-
"name": "project",
828-
"label": "Jira Project",
829-
"choices": [(p["id"], p["key"]) for p in jira_projects],
830-
"default": meta["id"],
831-
"type": "select",
832-
"updatesForm": True,
833-
},
847+
projects_form_field,
834848
*fields,
835849
{
836850
"name": "issuetype",

Diff for: tests/sentry/integrations/jira/test_integration.py

+83-4
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,10 @@ def test_get_create_issue_config(self):
112112
"name": "project",
113113
"default": "10000",
114114
"updatesForm": True,
115-
"choices": [("10000", "EX"), ("10001", "ABC")],
115+
"choices": [("10000", "EX - Example"), ("10001", "ABC - Alphabetical")],
116116
"label": "Jira Project",
117117
"type": "select",
118+
"required": True,
118119
},
119120
{
120121
"default": "message",
@@ -208,6 +209,81 @@ def test_get_create_issue_config(self):
208209
},
209210
]
210211

212+
@responses.activate
213+
@with_feature("organizations:jira-paginated-projects")
214+
def test_get_create_issue_config_paginated_projects(self):
215+
"""Test that projects are fetched using pagination when the feature flag is enabled"""
216+
event = self.store_event(
217+
data={
218+
"event_id": "a" * 32,
219+
"message": "message",
220+
"timestamp": self.min_ago,
221+
},
222+
project_id=self.project.id,
223+
default_event_type=EventType.DEFAULT,
224+
)
225+
group = event.group
226+
assert group is not None
227+
228+
# Mock the paginated projects response
229+
responses.add(
230+
responses.GET,
231+
"https://example.atlassian.net/rest/api/2/project/search",
232+
json={
233+
"values": [
234+
{"id": "10000", "key": "PROJ1", "name": "Project 1"},
235+
{"id": "10001", "key": "PROJ2", "name": "Project 2"},
236+
],
237+
"total": 2,
238+
},
239+
)
240+
241+
# Mock the create issue metadata endpoint
242+
responses.add(
243+
responses.GET,
244+
"https://example.atlassian.net/rest/api/2/issue/createmeta",
245+
json={
246+
"projects": [
247+
{
248+
"id": "10000",
249+
"key": "PROJ1",
250+
"name": "Project 1",
251+
"issuetypes": [
252+
{
253+
"description": "An error in the code",
254+
"fields": {
255+
"issuetype": {
256+
"key": "issuetype",
257+
"name": "Issue Type",
258+
"required": True,
259+
}
260+
},
261+
"id": "bug1",
262+
"name": "Bug",
263+
}
264+
],
265+
}
266+
]
267+
},
268+
)
269+
270+
installation = self.integration.get_installation(self.organization.id)
271+
fields = installation.get_create_issue_config(group, self.user)
272+
273+
# Find the project field in the config
274+
project_field = next(field for field in fields if field["name"] == "project")
275+
276+
# Verify the project field is configured correctly
277+
assert (
278+
project_field["url"]
279+
== f"/extensions/jira/search/{self.organization.slug}/{self.integration.id}/"
280+
)
281+
assert project_field["choices"] == [
282+
("10000", "PROJ1 - Project 1"),
283+
("10001", "PROJ2 - Project 2"),
284+
]
285+
assert project_field["type"] == "select"
286+
211287
def test_get_create_issue_config_customer_domain(self):
212288
event = self.store_event(
213289
data={
@@ -365,11 +441,12 @@ def test_get_create_issue_config_with_default_and_param(self):
365441

366442
assert project_field == {
367443
"default": "10000",
368-
"choices": [("10000", "EX"), ("10001", "ABC")],
444+
"choices": [("10000", "EX - Example"), ("10001", "ABC - Alphabetical")],
369445
"type": "select",
370446
"name": "project",
371447
"label": "Jira Project",
372448
"updatesForm": True,
449+
"required": True,
373450
}
374451

375452
def test_get_create_issue_config_with_default(self):
@@ -398,11 +475,12 @@ def test_get_create_issue_config_with_default(self):
398475

399476
assert project_field == {
400477
"default": "10001",
401-
"choices": [("10000", "EX"), ("10001", "ABC")],
478+
"choices": [("10000", "EX - Example"), ("10001", "ABC - Alphabetical")],
402479
"type": "select",
403480
"name": "project",
404481
"label": "Jira Project",
405482
"updatesForm": True,
483+
"required": True,
406484
}
407485

408486
@patch("sentry.integrations.jira.integration.JiraIntegration.fetch_issue_create_meta")
@@ -447,11 +525,12 @@ def test_get_create_issue_config_with_default_project_deleted(
447525

448526
assert project_field == {
449527
"default": "10001",
450-
"choices": [("10000", "EX"), ("10001", "ABC")],
528+
"choices": [("10000", "EX - Example"), ("10001", "ABC - Alphabetical")],
451529
"type": "select",
452530
"name": "project",
453531
"label": "Jira Project",
454532
"updatesForm": True,
533+
"required": True,
455534
}
456535

457536
def test_get_create_issue_config_with_label_default(self):

Diff for: tests/sentry/integrations/jira/test_search_endpoint.py

+47
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from fixtures.integrations.stub_service import StubService
88
from sentry.testutils.cases import APITestCase
9+
from sentry.testutils.helpers.features import with_feature
910
from sentry.testutils.silo import control_silo_test
1011

1112

@@ -181,3 +182,49 @@ def test_customfield_search_error(self):
181182
assert resp.data == {
182183
"detail": "Unable to fetch autocomplete for customfield_0123 from Jira"
183184
}
185+
186+
@responses.activate
187+
@with_feature("organizations:jira-paginated-projects")
188+
def test_project_search_with_pagination(self):
189+
responses.add(
190+
responses.GET,
191+
"https://example.atlassian.net/rest/api/2/project/search",
192+
json={
193+
"values": [
194+
{"id": "10000", "key": "EX", "name": "Example"},
195+
],
196+
"total": 2,
197+
},
198+
)
199+
200+
self.login_as(self.user)
201+
202+
path = reverse(
203+
"sentry-extensions-jira-search", args=[self.organization.slug, self.integration.id]
204+
)
205+
206+
resp = self.client.get(f"{path}?field=project&query=example")
207+
assert resp.status_code == 200
208+
assert resp.data == [
209+
{"label": "EX - Example", "value": "10000"},
210+
]
211+
212+
@responses.activate
213+
@with_feature("organizations:jira-paginated-projects")
214+
def test_project_search_error_with_pagination(self):
215+
responses.add(
216+
responses.GET,
217+
"https://example.atlassian.net/rest/api/2/project/search",
218+
status=500,
219+
body="susge",
220+
)
221+
222+
self.login_as(self.user)
223+
224+
path = reverse(
225+
"sentry-extensions-jira-search", args=[self.organization.slug, self.integration.id]
226+
)
227+
228+
resp = self.client.get(f"{path}?field=project&query=example")
229+
assert resp.status_code == 400
230+
assert resp.data == {"detail": "Unable to fetch projects from Jira"}

0 commit comments

Comments
 (0)