From 076c0e5708c82a915f0a5b3f56949a0aee7dd2a0 Mon Sep 17 00:00:00 2001 From: Christinarlong Date: Thu, 6 Aug 2026 13:07:41 -0700 Subject: [PATCH] catch apierror and display the project mappings anyway --- src/sentry/integrations/jira/integration.py | 12 +++- .../integrations/utils/atlassian_connect.py | 2 +- .../integrations/jira/test_integration.py | 62 +++++++++++++++++++ .../utils/test_atlassian_connect.py | 11 ++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/sentry/integrations/jira/integration.py b/src/sentry/integrations/jira/integration.py index aa417baf8f9f..308624eb3ccc 100644 --- a/src/sentry/integrations/jira/integration.py +++ b/src/sentry/integrations/jira/integration.py @@ -549,7 +549,17 @@ def get_config_data(self): ) sync_status_forward = {} - project_mappings = self._filter_active_projects(project_mappings) + try: + project_mappings = self._filter_active_projects(project_mappings) + except ApiError as e: + logger.info( + "jira.get-config-data.filter-active-projects-failed", + extra={ + "org_id": self.organization_id, + "integration_id": self.model.id, + "error": str(e), + }, + ) for pm in project_mappings: sync_status_forward[pm.external_id] = { diff --git a/src/sentry/integrations/utils/atlassian_connect.py b/src/sentry/integrations/utils/atlassian_connect.py index 0d55b36c081c..3eb822ddb544 100644 --- a/src/sentry/integrations/utils/atlassian_connect.py +++ b/src/sentry/integrations/utils/atlassian_connect.py @@ -36,7 +36,7 @@ def get_query_hash( if isinstance(v, str): param_val = percent_encode(v) else: - param_val = ",".join(percent_encode(val) for val in v) + param_val = ",".join(percent_encode(val) for val in sorted(v)) sorted_query.append(f"{percent_encode(k)}={param_val}") query_string = "{}&{}&{}".format(method, uri, "&".join(sorted_query)) diff --git a/tests/sentry/integrations/jira/test_integration.py b/tests/sentry/integrations/jira/test_integration.py index c899e9113e51..18c75c9035ff 100644 --- a/tests/sentry/integrations/jira/test_integration.py +++ b/tests/sentry/integrations/jira/test_integration.py @@ -19,6 +19,7 @@ from sentry.integrations.pipeline import IntegrationPipeline from sentry.integrations.services.integration import integration_service from sentry.shared_integrations.exceptions import ( + ApiError, IntegrationConfigurationError, IntegrationError, IntegrationFormError, @@ -1263,6 +1264,67 @@ def test_get_config_data(self) -> None: assert installation.org_integration is not None assert installation.org_integration.config["sync_status_forward"] is True + def test_get_config_data_returns_mappings_when_jira_api_fails(self) -> None: + integration = self.create_provider_integration( + provider="jira", + name="Example Jira", + metadata={ + "oauth_client_id": "oauth-client-id", + "shared_secret": "a-super-secret-key-from-atlassian", + "base_url": "https://example.atlassian.net", + "domain_name": "example.atlassian.net", + }, + ) + integration.add_organization(self.organization, self.user) + + org_integration = OrganizationIntegration.objects.get( + organization_id=self.organization.id, integration_id=integration.id + ) + + org_integration.config = { + "sync_comments": True, + "sync_forward_assignment": True, + "sync_reverse_assignment": True, + "sync_status_reverse": True, + "sync_status_forward": True, + } + org_integration.save() + + self.create_integration_external_project( + organization_id=self.organization.id, + integration_id=integration.id, + external_id="12345", + unresolved_status="in_progress", + resolved_status="done", + ) + + self.create_integration_external_project( + organization_id=self.organization.id, + integration_id=integration.id, + external_id="67890", + unresolved_status="todo", + resolved_status="resolved", + ) + + installation = integration.get_installation(self.organization.id) + + with mock.patch.object( + installation, "_filter_active_projects", side_effect=ApiError("Jira is down") + ): + config = installation.get_config_data() + + assert config == { + "sync_comments": True, + "sync_forward_assignment": True, + "sync_reverse_assignment": True, + "sync_status_reverse": True, + "sync_status_forward": { + "12345": {"on_resolve": "done", "on_unresolve": "in_progress"}, + "67890": {"on_resolve": "resolved", "on_unresolve": "todo"}, + }, + "issues_ignored_fields": "", + } + @responses.activate def test_get_config_data_filters_via_paginated_endpoint_with_flag(self) -> None: integration = self.create_provider_integration( diff --git a/tests/sentry/integrations/utils/test_atlassian_connect.py b/tests/sentry/integrations/utils/test_atlassian_connect.py index a8bd854215ce..605106789dfe 100644 --- a/tests/sentry/integrations/utils/test_atlassian_connect.py +++ b/tests/sentry/integrations/utils/test_atlassian_connect.py @@ -61,6 +61,17 @@ def test_get_query_hash(self) -> None: result = get_query_hash(uri=self.path, method=self.method, query_params=self.query_params) assert result == self.query_hash + def test_get_query_hash_sorts_multi_value_params(self) -> None: + uri = "/rest/api/2/project/search" + method = "GET" + result_unsorted = get_query_hash( + uri=uri, method=method, query_params={"id": ["10002", "10000", "10001"]} + ) + result_sorted = get_query_hash( + uri=uri, method=method, query_params={"id": ["10000", "10001", "10002"]} + ) + assert result_unsorted == result_sorted + def test_get_integration_from_jwt_success(self) -> None: integration = get_integration_from_jwt( token=self.valid_jwt,