Skip to content

Commit 12bb4a9

Browse files
authored
ref(slack): Refactor Link Identity View (getsentry#72792)
https://www.notion.so/sentry/Slack-ff5b8ab4fd1e4760ab829438ce97ce15 Addresses `bot-3` task. Here, I refactored the view to have two separate methods for get and post. I added bunch of logging and metrics & improved tests.
1 parent e7ab023 commit 12bb4a9

File tree

4 files changed

+157
-34
lines changed

4 files changed

+157
-34
lines changed

src/sentry/integrations/slack/views/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Any
22

3-
from django.http import HttpResponse
3+
from django.http import HttpRequest, HttpResponse
44
from django.urls import reverse
55
from django.views.decorators.cache import never_cache as django_never_cache
66
from rest_framework.request import Request
@@ -23,7 +23,7 @@ def build_linking_url(endpoint: str, **kwargs: Any) -> str:
2323
return url
2424

2525

26-
def render_error_page(request: Request, status: int, body_text: str) -> HttpResponse:
26+
def render_error_page(request: Request | HttpRequest, status: int, body_text: str) -> HttpResponse:
2727
return render_to_response(
2828
"sentry/integrations/slack/link-team-error.html",
2929
request=request,
Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1+
import logging
2+
13
from django.core.signing import BadSignature, SignatureExpired
2-
from django.http import HttpResponse
4+
from django.db import IntegrityError
5+
from django.http import Http404, HttpRequest, HttpResponse
6+
from django.http.response import HttpResponseBase
37
from django.utils.decorators import method_decorator
48
from rest_framework.request import Request
59

10+
from sentry.integrations.slack.views import render_error_page
11+
from sentry.integrations.slack.views.types import IdentityParams
612
from sentry.integrations.types import ExternalProviderEnum, ExternalProviders
713
from sentry.integrations.utils import get_identity_or_404
814
from sentry.models.identity import Identity
915
from sentry.notifications.notificationcontroller import NotificationController
1016
from sentry.notifications.notifications.integration_nudge import IntegrationNudgeNotification
1117
from sentry.services.hybrid_cloud.integration.model import RpcIntegration
18+
from sentry.utils import metrics
1219
from sentry.utils.signing import unsign
1320
from sentry.web.frontend.base import BaseView, control_silo_view
1421
from sentry.web.helpers import render_to_response
@@ -17,6 +24,8 @@
1724
from . import build_linking_url as base_build_linking_url
1825
from . import never_cache
1926

27+
_logger = logging.getLogger(__name__)
28+
2029
SUCCESS_LINKED_MESSAGE = (
2130
"Your Slack identity has been linked to your Sentry account. You're good to go!"
2231
)
@@ -40,47 +49,112 @@ class SlackLinkIdentityView(BaseView):
4049
Django view for linking user to slack account. Creates an entry on Identity table.
4150
"""
4251

52+
_METRICS_SUCCESS_KEY = "sentry.integrations.slack.link_identity_view.success"
53+
_METRICS_FAILURE_KEY = "sentry.integrations.slack.link_identity_view.failure"
54+
4355
@method_decorator(never_cache)
44-
def handle(self, request: Request, signed_params: str) -> HttpResponse:
56+
def dispatch(self, request: HttpRequest, signed_params: str) -> HttpResponseBase:
4557
try:
4658
params = unsign(signed_params)
47-
except (SignatureExpired, BadSignature):
59+
except (SignatureExpired, BadSignature) as e:
60+
_logger.warning("dispatch.signature_error", exc_info=e)
61+
metrics.incr(self._METRICS_FAILURE_KEY, tags={"error": str(e)}, sample_rate=1.0)
4862
return render_to_response(
4963
"sentry/integrations/slack/expired-link.html",
5064
request=request,
5165
)
5266

53-
organization, integration, idp = get_identity_or_404(
54-
ExternalProviders.SLACK,
55-
request.user,
56-
integration_id=params["integration_id"],
67+
try:
68+
organization, integration, idp = get_identity_or_404(
69+
ExternalProviders.SLACK,
70+
request.user,
71+
integration_id=params["integration_id"],
72+
)
73+
except Http404:
74+
_logger.exception(
75+
"get_identity_error", extra={"integration_id": params["integration_id"]}
76+
)
77+
metrics.incr(self._METRICS_FAILURE_KEY + ".get_identity", sample_rate=1.0)
78+
return render_error_page(
79+
request,
80+
status=404,
81+
body_text="HTTP 404: Could not find the Slack identity.",
82+
)
83+
84+
_logger.info("get_identity_success", extra={"integration_id": params["integration_id"]})
85+
metrics.incr(self._METRICS_SUCCESS_KEY + ".get_identity", sample_rate=1.0)
86+
params.update({"organization": organization, "integration": integration, "idp": idp})
87+
return super().dispatch(
88+
request, organization=organization, integration=integration, idp=idp, params=params
5789
)
5890

59-
if request.method != "POST":
60-
return render_to_response(
61-
"sentry/auth-link-identity.html",
62-
request=request,
63-
context={"organization": organization, "provider": integration.get_provider()},
91+
def get(self, request: Request, *args, **kwargs) -> HttpResponse:
92+
params = kwargs["params"]
93+
organization, integration = params["organization"], params["integration"]
94+
95+
return render_to_response(
96+
"sentry/auth-link-identity.html",
97+
request=request,
98+
context={"organization": organization, "provider": integration.get_provider()},
99+
)
100+
101+
def post(self, request: Request, *args, **kwargs) -> HttpResponse:
102+
try:
103+
params_dict = kwargs["params"]
104+
params = IdentityParams(
105+
organization=kwargs["organization"],
106+
integration=kwargs["integration"],
107+
idp=kwargs["idp"],
108+
slack_id=params_dict["slack_id"],
109+
channel_id=params_dict["channel_id"],
110+
)
111+
except KeyError as e:
112+
_logger.exception("slack.link.missing_params")
113+
metrics.incr(
114+
self._METRICS_FAILURE_KEY + ".post.missing_params",
115+
tags={"error": str(e)},
116+
sample_rate=1.0,
117+
)
118+
return render_error_page(
119+
request,
120+
status=400,
121+
body_text="HTTP 400: Missing required parameters.",
64122
)
65123

66-
Identity.objects.link_identity(user=request.user, idp=idp, external_id=params["slack_id"])
124+
try:
125+
Identity.objects.link_identity(
126+
user=request.user, idp=params.idp, external_id=params.slack_id
127+
)
128+
except IntegrityError:
129+
_logger.exception("slack.link.integrity_error")
130+
metrics.incr(
131+
self._METRICS_FAILURE_KEY + ".post.identity.integrity_error",
132+
sample_rate=1.0,
133+
)
134+
raise Http404
135+
136+
# TODO: We should use use the dataclass to send the slack response
137+
send_slack_response(
138+
params.integration, SUCCESS_LINKED_MESSAGE, params.__dict__, command="link"
139+
)
67140

68-
send_slack_response(integration, SUCCESS_LINKED_MESSAGE, params, command="link")
69-
has_slack_settings = None
70141
controller = NotificationController(
71142
recipients=[request.user],
72-
organization_id=organization.id,
143+
organization_id=params.organization.id,
73144
provider=ExternalProviderEnum.SLACK,
74145
)
75146
has_slack_settings = controller.user_has_any_provider_settings(ExternalProviderEnum.SLACK)
76147

77148
if not has_slack_settings:
78-
IntegrationNudgeNotification(organization, request.user, ExternalProviders.SLACK).send()
149+
IntegrationNudgeNotification(
150+
params.organization, request.user, ExternalProviders.SLACK
151+
).send()
152+
153+
_logger.info("link_identity_success", extra={"slack_id": params.slack_id})
154+
metrics.incr(self._METRICS_SUCCESS_KEY + ".post.link_identity", sample_rate=1.0)
79155

80-
# TODO(epurkhiser): We could do some fancy slack querying here to
81-
# render a nice linking page with info about the user their linking.
82156
return render_to_response(
83157
"sentry/integrations/slack/linked.html",
84158
request=request,
85-
context={"channel_id": params["channel_id"], "team_id": integration.external_id},
159+
context={"channel_id": params.channel_id, "team_id": params.integration.external_id},
86160
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from dataclasses import dataclass
2+
3+
from sentry.models.identity import IdentityProvider
4+
from sentry.models.integrations.integration import Integration
5+
from sentry.services.hybrid_cloud.organization import RpcOrganization
6+
7+
8+
@dataclass
9+
class IdentityParams:
10+
organization: RpcOrganization
11+
integration: Integration
12+
idp: IdentityProvider
13+
slack_id: str
14+
channel_id: str
15+
response_url: str | None = None
16+
17+
def __init__(self, organization, integration, idp, slack_id, channel_id, response_url=None):
18+
self.organization = organization
19+
self.integration = integration
20+
self.idp = idp
21+
self.slack_id = slack_id
22+
self.channel_id = channel_id
23+
self.response_url = response_url

src/sentry/integrations/slack/views/unlink_identity.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from django.http import Http404, HttpRequest, HttpResponse
66
from django.http.response import HttpResponseBase
77
from django.utils.decorators import method_decorator
8+
from rest_framework.request import Request
89

910
from sentry.integrations.slack.utils.notifications import send_slack_response
1011
from sentry.integrations.slack.views import build_linking_url as base_build_linking_url
11-
from sentry.integrations.slack.views import never_cache
12+
from sentry.integrations.slack.views import never_cache, render_error_page
13+
from sentry.integrations.slack.views.types import IdentityParams
1214
from sentry.integrations.types import ExternalProviders
1315
from sentry.integrations.utils import get_identity_or_404
1416
from sentry.models.identity import Identity
@@ -66,14 +68,20 @@ def dispatch(self, request: HttpRequest, signed_params: str) -> HttpResponseBase
6668
"get_identity_error", extra={"integration_id": params["integration_id"]}
6769
)
6870
metrics.incr(self._METRICS_FAILURE_KEY + ".get_identity", sample_rate=1.0)
69-
raise
71+
return render_error_page(
72+
request,
73+
status=404,
74+
body_text="HTTP 404: Could not find the Slack identity.",
75+
)
7076

7177
_logger.info("get_identity_success", extra={"integration_id": params["integration_id"]})
7278
metrics.incr(self._METRICS_SUCCESS_KEY + ".get_identity", sample_rate=1.0)
7379
params.update({"organization": organization, "integration": integration, "idp": idp})
74-
return super().dispatch(request, params=params)
80+
return super().dispatch(
81+
request, organization=organization, integration=integration, idp=idp, params=params
82+
)
7583

76-
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
84+
def get(self, request: Request, *args, **kwargs) -> HttpResponse:
7785
params = kwargs["params"]
7886
organization, integration = params["organization"], params["integration"]
7987

@@ -83,27 +91,45 @@ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
8391
context={"organization": organization, "provider": integration.get_provider()},
8492
)
8593

86-
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
87-
params = kwargs["params"]
88-
integration, idp = params["integration"], params["idp"]
94+
def post(self, request: Request, *args, **kwargs) -> HttpResponse:
95+
try:
96+
params_dict = kwargs["params"]
97+
params = IdentityParams(
98+
organization=kwargs["organization"],
99+
integration=kwargs["integration"],
100+
idp=kwargs["idp"],
101+
slack_id=params_dict["slack_id"],
102+
channel_id=params_dict["channel_id"],
103+
)
104+
except KeyError as e:
105+
_logger.exception("slack.unlink.missing_params", extra={"error": str(e)})
106+
metrics.incr(self._METRICS_FAILURE_KEY + ".post.missing_params", sample_rate=1.0)
107+
return render_error_page(
108+
request,
109+
status=400,
110+
body_text="HTTP 400: Missing required parameters.",
111+
)
89112

90113
try:
91-
Identity.objects.filter(idp_id=idp.id, external_id=params["slack_id"]).delete()
114+
Identity.objects.filter(idp_id=params.idp, external_id=params.slack_id).delete()
92115
except IntegrityError:
93-
_logger.exception("slack.unlink.integrity-error")
116+
_logger.exception("slack.unlink.integrity_error")
94117
metrics.incr(
95118
self._METRICS_FAILURE_KEY + ".post.identity.integrity_error",
96119
sample_rate=1.0,
97120
)
98121
raise Http404
99122

100-
send_slack_response(integration, SUCCESS_UNLINKED_MESSAGE, params, command="unlink")
123+
# TODO: We should use use the dataclass to send the slack response
124+
send_slack_response(
125+
params.integration, SUCCESS_UNLINKED_MESSAGE, params.__dict__, command="unlink"
126+
)
101127

102-
_logger.info("unlink_identity_success", extra={"slack_id": params["slack_id"]})
128+
_logger.info("unlink_identity_success", extra={"slack_id": params.slack_id})
103129
metrics.incr(self._METRICS_SUCCESS_KEY + ".post.unlink_identity", sample_rate=1.0)
104130

105131
return render_to_response(
106132
"sentry/integrations/slack/unlinked.html",
107133
request=request,
108-
context={"channel_id": params["channel_id"], "team_id": integration.external_id},
134+
context={"channel_id": params.channel_id, "team_id": params.integration.external_id},
109135
)

0 commit comments

Comments
 (0)