Skip to content
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

chore(sentry apps): New error design for external request flow #83678

Open
wants to merge 19 commits into
base: master
Choose a base branch
from

Conversation

Christinarlong
Copy link
Contributor

No description provided.

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Jan 17, 2025
Copy link

codecov bot commented Jan 17, 2025

❌ 11 Tests Failed:

Tests completed Failed Passed Skipped
109 11 98 2
View the top 3 failed tests by shortest run time
::tests.sentry.api.endpoints.test_system_options
Stack Traces | 0s run time
#x1B[1m#x1B[.../api/endpoints/test_system_options.py#x1B[0m:9: in <module>
    class SystemOptionsTest(APITestCase):
#x1B[1m#x1B[.../api/endpoints/test_system_options.py#x1B[0m:10: in SystemOptionsTest
    url = reverse("sentry-api-0-system-options")
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/base.py#x1B[0m:88: in reverse
    return resolver._reverse_with_prefix(view, prefix, *args, **kwargs)
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/resolvers.py#x1B[0m:749: in _reverse_with_prefix
    self._populate()
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/resolvers.py#x1B[0m:548: in _populate
    for url_pattern in reversed(self.url_patterns):
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/utils/functional.py#x1B[0m:47: in __get__
    res = instance.__dict__[self.name] = self.func(instance)
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/resolvers.py#x1B[0m:718: in url_patterns
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/utils/functional.py#x1B[0m:47: in __get__
    res = instance.__dict__[self.name] = self.func(instance)
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/resolvers.py#x1B[0m:711: in urlconf_module
    return import_module(self.urlconf_name)
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/importlib/__init__.py#x1B[0m:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:1387: in _gcd_import
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:1360: in _find_and_load
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:1331: in _find_and_load_unlocked
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:935: in _load_unlocked
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap_external>#x1B[0m:1026: in exec_module
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:488: in _call_with_frames_removed
    ???
#x1B[1m#x1B[.../sentry/conf/urls.py#x1B[0m:3: in <module>
    from sentry.web.urls import urlpatterns
#x1B[1m#x1B[.../sentry/web/urls.py#x1B[0m:168: in <module>
    include("sentry.api.urls"),
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/conf.py#x1B[0m:39: in include
    urlconf_module = import_module(urlconf_module)
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/importlib/__init__.py#x1B[0m:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
#x1B[1m#x1B[.../sentry/api/urls.py#x1B[0m:289: in <module>
    from sentry.sentry_apps.api.endpoints.installation_external_issue_actions import (
#x1B[1m#x1B[.../api/endpoints/installation_external_issue_actions.py#x1B[0m:36: in <module>
    class SentryAppInstallationExternalIssueActionsSerializer(serializers.Serizlizer):
#x1B[1m#x1B[31mE   AttributeError: module 'rest_framework.serializers' has no attribute 'Serizlizer'. Did you mean: 'Serializer'?#x1B[0m
::tests.sentry.hybridcloud.apigateway.test_apigateway
Stack Traces | 0s run time
#x1B[1m#x1B[.../hybridcloud/apigateway/test_apigateway.py#x1B[0m:11: in <module>
    from sentry.testutils.helpers.apigateway import ApiGatewayTestCase, verify_request_params
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:1360: in _find_and_load
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:1331: in _find_and_load_unlocked
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:935: in _load_unlocked
    ???
#x1B[1m#x1B[31m.venv/lib/python3.13.../_pytest/assertion/rewrite.py#x1B[0m:178: in exec_module
    exec(co, module.__dict__)
#x1B[1m#x1B[.../testutils/helpers/apigateway.py#x1B[0m:13: in <module>
    import sentry.api.urls as api_urls
#x1B[1m#x1B[.../sentry/api/urls.py#x1B[0m:289: in <module>
    from sentry.sentry_apps.api.endpoints.installation_external_issue_actions import (
#x1B[1m#x1B[.../api/endpoints/installation_external_issue_actions.py#x1B[0m:36: in <module>
    class SentryAppInstallationExternalIssueActionsSerializer(serializers.Serizlizer):
#x1B[1m#x1B[31mE   AttributeError: module 'rest_framework.serializers' has no attribute 'Serizlizer'. Did you mean: 'Serializer'?#x1B[0m
::tests.sentry.middleware.integrations.parsers.test_github_enterprise
Stack Traces | 0s run time
#x1B[1m#x1B[.../integrations/parsers/test_github_enterprise.py#x1B[0m:24: in <module>
    class GithubEnterpriseRequestParserTest(TestCase):
#x1B[1m#x1B[.../integrations/parsers/test_github_enterprise.py#x1B[0m:26: in GithubEnterpriseRequestParserTest
    path = reverse("sentry-integration-github-enterprise-webhook")
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/base.py#x1B[0m:88: in reverse
    return resolver._reverse_with_prefix(view, prefix, *args, **kwargs)
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/resolvers.py#x1B[0m:749: in _reverse_with_prefix
    self._populate()
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/resolvers.py#x1B[0m:548: in _populate
    for url_pattern in reversed(self.url_patterns):
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/utils/functional.py#x1B[0m:47: in __get__
    res = instance.__dict__[self.name] = self.func(instance)
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/resolvers.py#x1B[0m:718: in url_patterns
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/utils/functional.py#x1B[0m:47: in __get__
    res = instance.__dict__[self.name] = self.func(instance)
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/resolvers.py#x1B[0m:711: in urlconf_module
    return import_module(self.urlconf_name)
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/importlib/__init__.py#x1B[0m:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:1387: in _gcd_import
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:1360: in _find_and_load
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:1331: in _find_and_load_unlocked
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:935: in _load_unlocked
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap_external>#x1B[0m:1026: in exec_module
    ???
#x1B[1m#x1B[31m<frozen importlib._bootstrap>#x1B[0m:488: in _call_with_frames_removed
    ???
#x1B[1m#x1B[.../sentry/conf/urls.py#x1B[0m:3: in <module>
    from sentry.web.urls import urlpatterns
#x1B[1m#x1B[.../sentry/web/urls.py#x1B[0m:168: in <module>
    include("sentry.api.urls"),
#x1B[1m#x1B[31m.venv/lib/python3.13.../django/urls/conf.py#x1B[0m:39: in include
    urlconf_module = import_module(urlconf_module)
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/importlib/__init__.py#x1B[0m:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
#x1B[1m#x1B[.../sentry/api/urls.py#x1B[0m:289: in <module>
    from sentry.sentry_apps.api.endpoints.installation_external_issue_actions import (
#x1B[1m#x1B[.../api/endpoints/installation_external_issue_actions.py#x1B[0m:36: in <module>
    class SentryAppInstallationExternalIssueActionsSerializer(serializers.Serizlizer):
#x1B[1m#x1B[31mE   AttributeError: module 'rest_framework.serializers' has no attribute 'Serizlizer'. Did you mean: 'Serializer'?#x1B[0m

To view more test analytics, go to the Test Analytics Dashboard
📢 Thoughts on this report? Let us know!

Base automatically changed from crl/new-sa-error-w-fixes to master January 17, 2025 21:40
).run()

assert (
str(exception_info.value) == "Missing `value` or `label` in option data for SelectField"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are duplicate tests that test cases already covered in prev ones

@Christinarlong Christinarlong marked this pull request as ready for review January 21, 2025 18:49
@Christinarlong Christinarlong requested review from a team as code owners January 21, 2025 18:49
Comment on lines -77 to -86
except (APIError, ValidationError, APIUnauthorized) as e:
return Response({"error": str(e)}, status=400)
except Exception as e:
error_id = capture_exception(e)
return Response(
{
"error": f"Something went wrong while trying to link issue. Sentry error ID: {error_id}"
},
status=500,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason we're no longer capturing these errors?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error handler in the base IntegrationPlatformEndpoint (link) will do the catching for us

Comment on lines -45 to -55
except ValidationError as e:
return Response(
{"error": e.message},
status=400,
)
except APIError:
message = f'Error retrieving select field options from {request.GET.get("uri")}'
return Response(
{"error": message},
status=400,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, any reason we're not capturing these anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

},
status_code=503,
Copy link
Member

@cathteng cathteng Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should a validation error raise 503? i think this should be 500 if we can't extract the json

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

503 was mainly chosen to represent some failure that can't be fixed on the client side because of an issue due to the third party. Though I couldn't really find a status code that matched cleanly with the 3p error case, so open to suggestions.

For the 500 code, if we can't extract the JSON, I assumed that meant the 3p was giving back some bad format like HTML or smthn. In that case the error isn't really Sentry's fault, which I think 500 represents something went wrong with Sentry. 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if i am understanding this correctly, this error would be when the 3p sends us an invalid payload? could we return 400 then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

400 is a client error code though. My understanding is that for client error codes/4XXs, indicate the "client"/person who submitted the request did something wrong and that if they change their request they can get back a 200. A la they can correct the mistake themselves.

In this case though, if a 3p is not sending us the right format like HTML or smthn for a request, it's unlikely the end user will be able to correct the situation themselves by filling out say the form a different way.

raise SentryAppIntegratorError(
message=f"Issue occured while trying to contact {self.sentry_app.slug} to link issue",
webhook_context={"error_type": error_type, **extras},
status_code=503,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be 500? normal exceptions that get raised and are uncaught by django error handling return 500

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can catch RequestExceptions here instead of all Exceptions. It's probably better to have the uncaughts go through so we can get alerts and fine tune. Alternatively, we could have other non RequestException exceptions go under SentryAppSentryError which would also return a 500 and be a bit nicer

raise SentryAppIntegratorError(
message=f"Something went wrong while getting options for Select FormField from {self.sentry_app.slug}",
webhook_context={"error_type": message, **extra},
status_code=503,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIError raises 400

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't really want a 400 here since it's a 3p request failure

},
status_code=503,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be a 400 if it's during validation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with 503, since the client can't fix the issue/ the 3p response was incorrect so the 3p has to fix.

Copy link
Member

@iamrajjoshi iamrajjoshi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious on how we decide the error codes. might be worth documenting the decisions in our docs

@@ -45,7 +43,9 @@ def post(self, request: Request, installation) -> Response:
data = request.data.copy()

if not {"groupId", "action", "uri"}.issubset(data.keys()):
return Response(status=400)
raise SentryAppError(
message="Missing groupId, action, uri in request body", status_code=400
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe we tell them exactly whats missing 🤔

},
status_code=503,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if i am understanding this correctly, this error would be when the 3p sends us an invalid payload? could we return 400 then?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Scope: Backend Automatically applied to PRs that change backend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants