Skip to content

impr(jira): switch to paginated project endpoint for jira issue config #89707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 18, 2025
2 changes: 2 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,8 @@ def register_temporary_features(manager: FeatureManager):
manager.add("organizations:streamlined-publishing-flow", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable per-project selection for Jira integration
manager.add("organizations:jira-per-project-statuses", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable using paginated projects endpoint for Jira integration
manager.add("organizations:jira-paginated-projects", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable Relay extracting logs from breadcrumbs for a project.
manager.add("projects:ourlogs-breadcrumb-extraction", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)

Expand Down
6 changes: 6 additions & 0 deletions src/sentry/integrations/jira/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class JiraCloudClient(ApiClient):
ISSUE_URL = "/rest/api/2/issue/%s"
META_URL = "/rest/api/2/issue/createmeta"
PRIORITIES_URL = "/rest/api/2/priority"
PROJECTS_PAGINATED_URL = "/rest/api/2/project/search"
PROJECT_URL = "/rest/api/2/project"
SEARCH_URL = "/rest/api/2/search/"
VERSIONS_URL = "/rest/api/2/project/%s/versions"
Expand Down Expand Up @@ -122,6 +123,11 @@ def create_comment(self, issue_key, comment):
def update_comment(self, issue_key, comment_id, comment):
return self.put(self.COMMENT_URL % (issue_key, comment_id), data={"body": comment})

def get_projects_paginated(self, params: dict[str, str | Any] | None = None):
response = self.get(self.PROJECTS_PAGINATED_URL, params=params)
return response

# deprecated - please use paginated one above
def get_projects_list(self):
return self.get_cached(self.PROJECT_URL)

Expand Down
12 changes: 12 additions & 0 deletions src/sentry/integrations/jira/endpoints/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import features
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import control_silo_endpoint
Expand Down Expand Up @@ -83,6 +84,17 @@ def get(
)
users = [{"value": user_id, "label": display} for user_id, display in user_tuples]
return Response(users)
if features.has("organizations:jira-paginated-projects", organization, actor=request.user):
if field == "project":
try:
response = jira_client.get_projects_paginated(params={"query": query})
projects = [
{"label": f"{p["key"]} - {p["name"]}", "value": p["id"]}
for p in response.get("values", [])
]
except (ApiUnauthorized, ApiError):
return Response({"detail": "Unable to fetch projects from Jira"}, status=400)
return Response(projects)

try:
response = jira_client.get_field_autocomplete(name=field, value=query)
Expand Down
27 changes: 23 additions & 4 deletions src/sentry/integrations/jira/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,14 @@ def get_create_issue_config(self, group: Group | None, user: RpcUser, **kwargs):
project_id = params.get("project", defaults.get("project"))
client = self.get_client()
try:
jira_projects = client.get_projects_list()
if features.has(
"organizations:jira-paginated-projects", group.organization, actor=user
):
jira_projects = client.get_projects_paginated(
{"maxResults": MAX_PER_PROJECT_QUERIES}
)["values"]
else:
jira_projects = client.get_projects_list()
except ApiError as e:
logger.info(
"jira.get-create-issue-config.no-projects",
Expand Down Expand Up @@ -822,15 +829,27 @@ def get_create_issue_config(self, group: Group | None, user: RpcUser, **kwargs):
if not any(c for c in issue_type_choices if c[0] == issue_type):
issue_type = issue_type_meta["id"]

fields = [
projects_form_field: dict[str, Any] = {}
if features.has("organizations:jira-paginated-projects", group.organization, actor=user):
paginated_projects_url = reverse(
"sentry-extensions-jira-search", args=[self.organization.slug, self.model.id]
)
projects_form_field["url"] = paginated_projects_url

projects_form_field.update(
{
"name": "project",
"label": "Jira Project",
"choices": [(p["id"], p["key"]) for p in jira_projects],
"choices": [(p["id"], f"{p["key"]} - {p["name"]}") for p in jira_projects],
"default": meta["id"],
"type": "select",
"updatesForm": True,
},
"required": True,
}
)

fields = [
projects_form_field,
*fields,
{
"name": "issuetype",
Expand Down
87 changes: 83 additions & 4 deletions tests/sentry/integrations/jira/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@ def test_get_create_issue_config(self):
"name": "project",
"default": "10000",
"updatesForm": True,
"choices": [("10000", "EX"), ("10001", "ABC")],
"choices": [("10000", "EX - Example"), ("10001", "ABC - Alphabetical")],
"label": "Jira Project",
"type": "select",
"required": True,
},
{
"default": "message",
Expand Down Expand Up @@ -208,6 +209,81 @@ def test_get_create_issue_config(self):
},
]

@responses.activate
@with_feature("organizations:jira-paginated-projects")
def test_get_create_issue_config_paginated_projects(self):
"""Test that projects are fetched using pagination when the feature flag is enabled"""
event = self.store_event(
data={
"event_id": "a" * 32,
"message": "message",
"timestamp": self.min_ago,
},
project_id=self.project.id,
default_event_type=EventType.DEFAULT,
)
group = event.group
assert group is not None

# Mock the paginated projects response
responses.add(
responses.GET,
"https://example.atlassian.net/rest/api/2/project/search",
json={
"values": [
{"id": "10000", "key": "PROJ1", "name": "Project 1"},
{"id": "10001", "key": "PROJ2", "name": "Project 2"},
],
"total": 2,
},
)

# Mock the create issue metadata endpoint
responses.add(
responses.GET,
"https://example.atlassian.net/rest/api/2/issue/createmeta",
json={
"projects": [
{
"id": "10000",
"key": "PROJ1",
"name": "Project 1",
"issuetypes": [
{
"description": "An error in the code",
"fields": {
"issuetype": {
"key": "issuetype",
"name": "Issue Type",
"required": True,
}
},
"id": "bug1",
"name": "Bug",
}
],
}
]
},
)

installation = self.integration.get_installation(self.organization.id)
fields = installation.get_create_issue_config(group, self.user)

# Find the project field in the config
project_field = next(field for field in fields if field["name"] == "project")

# Verify the project field is configured correctly
assert (
project_field["url"]
== f"/extensions/jira/search/{self.organization.slug}/{self.integration.id}/"
)
assert project_field["choices"] == [
("10000", "PROJ1 - Project 1"),
("10001", "PROJ2 - Project 2"),
]
assert project_field["type"] == "select"

def test_get_create_issue_config_customer_domain(self):
event = self.store_event(
data={
Expand Down Expand Up @@ -365,11 +441,12 @@ def test_get_create_issue_config_with_default_and_param(self):

assert project_field == {
"default": "10000",
"choices": [("10000", "EX"), ("10001", "ABC")],
"choices": [("10000", "EX - Example"), ("10001", "ABC - Alphabetical")],
"type": "select",
"name": "project",
"label": "Jira Project",
"updatesForm": True,
"required": True,
}

def test_get_create_issue_config_with_default(self):
Expand Down Expand Up @@ -398,11 +475,12 @@ def test_get_create_issue_config_with_default(self):

assert project_field == {
"default": "10001",
"choices": [("10000", "EX"), ("10001", "ABC")],
"choices": [("10000", "EX - Example"), ("10001", "ABC - Alphabetical")],
"type": "select",
"name": "project",
"label": "Jira Project",
"updatesForm": True,
"required": True,
}

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

assert project_field == {
"default": "10001",
"choices": [("10000", "EX"), ("10001", "ABC")],
"choices": [("10000", "EX - Example"), ("10001", "ABC - Alphabetical")],
"type": "select",
"name": "project",
"label": "Jira Project",
"updatesForm": True,
"required": True,
}

def test_get_create_issue_config_with_label_default(self):
Expand Down
47 changes: 47 additions & 0 deletions tests/sentry/integrations/jira/test_search_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from fixtures.integrations.stub_service import StubService
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers.features import with_feature
from sentry.testutils.silo import control_silo_test


Expand Down Expand Up @@ -181,3 +182,49 @@ def test_customfield_search_error(self):
assert resp.data == {
"detail": "Unable to fetch autocomplete for customfield_0123 from Jira"
}

@responses.activate
@with_feature("organizations:jira-paginated-projects")
def test_project_search_with_pagination(self):
responses.add(
responses.GET,
"https://example.atlassian.net/rest/api/2/project/search",
json={
"values": [
{"id": "10000", "key": "EX", "name": "Example"},
],
"total": 2,
},
)

self.login_as(self.user)

path = reverse(
"sentry-extensions-jira-search", args=[self.organization.slug, self.integration.id]
)

resp = self.client.get(f"{path}?field=project&query=example")
assert resp.status_code == 200
assert resp.data == [
{"label": "EX - Example", "value": "10000"},
]

@responses.activate
@with_feature("organizations:jira-paginated-projects")
def test_project_search_error_with_pagination(self):
responses.add(
responses.GET,
"https://example.atlassian.net/rest/api/2/project/search",
status=500,
body="susge",
)

self.login_as(self.user)

path = reverse(
"sentry-extensions-jira-search", args=[self.organization.slug, self.integration.id]
)

resp = self.client.get(f"{path}?field=project&query=example")
assert resp.status_code == 400
assert resp.data == {"detail": "Unable to fetch projects from Jira"}
Loading