Skip to content

Commit e817470

Browse files
authored
ref(slack): Add APM to Slack and use use ApiClient (#18346)
* ref(slack): Add APM to Slack and use use ApiClient
1 parent 8761f03 commit e817470

File tree

11 files changed

+155
-93
lines changed

11 files changed

+155
-93
lines changed

src/sentry/api/endpoints/project_rule_details.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
from sentry.integrations.slack import tasks
1010
from sentry.mediators import project_rules
1111
from sentry.models import AuditLogEntryEvent, Rule, RuleStatus
12+
from sentry.web.decorators import transaction_start
1213

1314

1415
class ProjectRuleDetailsEndpoint(ProjectEndpoint):
1516
permission_classes = [ProjectSettingPermission]
1617

18+
@transaction_start("ProjectRuleDetailsEndpoint")
1719
def get(self, request, project, rule_id):
1820
"""
1921
Retrieve a rule
@@ -28,6 +30,7 @@ def get(self, request, project, rule_id):
2830
)
2931
return Response(serialize(rule, request.user))
3032

33+
@transaction_start("ProjectRuleDetailsEndpoint")
3134
def put(self, request, project, rule_id):
3235
"""
3336
Update a rule
@@ -81,6 +84,7 @@ def put(self, request, project, rule_id):
8184

8285
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
8386

87+
@transaction_start("ProjectRuleDetailsEndpoint")
8488
def delete(self, request, project, rule_id):
8589
"""
8690
Delete a rule

src/sentry/api/endpoints/project_rules.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
from sentry.mediators import project_rules
1212
from sentry.models import AuditLogEntryEvent, Rule, RuleStatus
1313
from sentry.signals import alert_rule_created
14+
from sentry.web.decorators import transaction_start
1415

1516

1617
class ProjectRulesEndpoint(ProjectEndpoint):
18+
@transaction_start("ProjectRulesEndpoint")
1719
def get(self, request, project):
1820
"""
1921
List a project's rules
@@ -34,6 +36,7 @@ def get(self, request, project):
3436
on_results=lambda x: serialize(x, request.user),
3537
)
3638

39+
@transaction_start("ProjectRulesEndpoint")
3740
def post(self, request, project):
3841
"""
3942
Create a rule

src/sentry/integrations/slack/action_endpoint.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
import six
44

55
from sentry import analytics
6-
from sentry import http
6+
77
from sentry.api import client
88
from sentry.api.base import Endpoint
99
from sentry.models import Group, Project, Identity, IdentityProvider, ApiKey
1010
from sentry.utils import json
11+
from sentry.web.decorators import transaction_start
12+
from sentry.shared_integrations.exceptions import ApiError
1113

14+
from .client import SlackClient
1215
from .link_identity import build_linking_url
1316
from .requests import SlackActionRequest, SlackRequestError
14-
from .utils import build_group_attachment, logger, track_response_code
17+
from .utils import build_group_attachment, logger
1518

1619
LINK_IDENTITY_MESSAGE = "Looks like you haven't linked your Sentry account with your Slack identity yet! <{associate_url}|Link your identity now> to perform actions in Sentry through Slack."
1720

@@ -120,13 +123,11 @@ def open_resolve_dialog(self, data, group, integration):
120123
"token": integration.metadata["access_token"],
121124
}
122125

123-
session = http.build_session()
124-
req = session.post("https://slack.com/api/dialog.open", data=payload)
125-
status_code = req.status_code
126-
resp = req.json()
127-
if not resp.get("ok"):
128-
logger.error("slack.action.response-error", extra={"response": resp})
129-
track_response_code(status_code, resp.get("ok"))
126+
slack_client = SlackClient()
127+
try:
128+
slack_client.post("/dialog.open", data=payload)
129+
except ApiError as e:
130+
logger.error("slack.action.response-error", extra={"error": six.text_type(e)})
130131

131132
def construct_reply(self, attachment, is_message=False):
132133
# XXX(epurkhiser): Slack is inconsistent about it's expected responses
@@ -149,6 +150,7 @@ def is_message(self, data):
149150
# posted messages will not have the type at all.
150151
return data.get("original_message", {}).get("type") == "message"
151152

153+
@transaction_start("SlackActionEndpoint")
152154
def post(self, request):
153155
logging_data = {}
154156

@@ -229,13 +231,14 @@ def post(self, request):
229231
)
230232

231233
# use the original response_url to update the link attachment
232-
session = http.build_session()
233-
req = session.post(slack_request.callback_data["orig_response_url"], json=body)
234-
status_code = req.status_code
235-
resp = req.json()
236-
if not resp.get("ok"):
237-
logger.error("slack.action.response-error", extra={"response": resp})
238-
track_response_code(status_code, resp.get("ok"))
234+
slack_client = SlackClient()
235+
try:
236+
slack_client.post(
237+
slack_request.callback_data["orig_response_url"], data=body, json=True
238+
)
239+
except ApiError as e:
240+
logger.error("slack.action.response-error", extra={"error": six.text_type(e)})
241+
239242
return self.respond()
240243

241244
# Usually we'll want to respond with the updated attachment including
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import absolute_import
2+
3+
import six
4+
5+
from sentry.integrations.client import ApiClient
6+
from sentry.shared_integrations.exceptions import ApiError
7+
from sentry.utils import metrics
8+
9+
SLACK_DATADOG_METRIC = "integrations.slack.http_response"
10+
11+
12+
class SlackClient(ApiClient):
13+
allow_redirects = False
14+
integration_name = "slack"
15+
base_url = "https://slack.com/api"
16+
datadog_prefix = "integrations.slack"
17+
18+
def track_response_data(self, code, span, error=None, resp=None):
19+
span.set_http_status(code)
20+
span.set_tag("integration", "slack")
21+
22+
is_ok = False
23+
# If Slack gives us back a 200 we still want to check the 'ok' param
24+
if resp:
25+
response = resp.json()
26+
is_ok = response.get("ok")
27+
span.set_tag("ok", is_ok)
28+
29+
# when 'ok' is False, we can add the error we get back as a tag
30+
if not is_ok:
31+
error = response.get("error")
32+
span.set_tag("slack_error", error)
33+
34+
metrics.incr(
35+
SLACK_DATADOG_METRIC, sample_rate=1.0, tags={"ok": is_ok, "status": code},
36+
)
37+
38+
extra = {
39+
self.integration_type: self.name,
40+
"status_string": six.text_type(code),
41+
"error": six.text_type(error)[:256] if error else None,
42+
}
43+
extra.update(getattr(self, "logging_context", None) or {})
44+
self.logger.info(u"%s.http_response" % (self.integration_type), extra=extra)
45+
46+
def request(self, method, path, headers=None, data=None, params=None, json=False, timeout=None):
47+
# TODO(meredith): Slack actually supports json now for the chat.postMessage so we
48+
# can update that so we don't have to pass json=False here
49+
response = self._request(method, path, headers=headers, data=data, params=params, json=json)
50+
if not response.json.get("ok"):
51+
raise ApiError(response.get("error", ""))
52+
return response

src/sentry/integrations/slack/event_endpoint.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77

88
from django.db.models import Q
99

10-
from sentry import http
1110
from sentry.api.base import Endpoint
1211
from sentry.incidents.models import Incident
1312
from sentry.models import Group, Project
13+
from sentry.shared_integrations.exceptions import ApiError
14+
from sentry.web.decorators import transaction_start
1415

16+
from .client import SlackClient
1517
from .requests import SlackEventRequest, SlackRequestError
16-
from .utils import build_group_attachment, build_incident_attachment, logger, track_response_code
18+
from .utils import build_group_attachment, build_incident_attachment, logger
1719

1820
# XXX(dcramer): this could be more tightly bound to our configured domain,
1921
# but slack limits what we can unfurl anyways so its probably safe
@@ -123,17 +125,16 @@ def on_link_shared(self, request, integration, token, data):
123125
"unfurls": json.dumps(results),
124126
}
125127

126-
session = http.build_session()
127-
req = session.post("https://slack.com/api/chat.unfurl", data=payload)
128-
status_code = req.status_code
129-
response = req.json()
130-
track_response_code(status_code, response.get("ok"))
131-
req.raise_for_status()
132-
if not response.get("ok"):
133-
logger.error("slack.event.unfurl-error", extra={"response": response})
128+
client = SlackClient()
129+
try:
130+
client.post("/chat.unfurl", data=payload)
131+
except ApiError as e:
132+
logger.error("slack.event.unfurl-error", extra={"error": six.text_type(e)})
133+
134134
return self.respond()
135135

136136
# TODO(dcramer): implement app_uninstalled and tokens_revoked
137+
@transaction_start("SlackEventEndpoint")
137138
def post(self, request):
138139
try:
139140
slack_request = SlackEventRequest(request)

src/sentry/integrations/slack/integration.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import absolute_import
22

3+
import six
4+
35
from django.utils.translation import ugettext_lazy as _
46
from django.conf import settings
57

6-
from sentry import http
78
from sentry.identity.pipeline import IdentityProviderPipeline
89
from sentry.integrations import (
910
IntegrationFeatures,
@@ -13,7 +14,10 @@
1314
)
1415
from sentry.pipeline import NestedPipelineView
1516
from sentry.utils.http import absolute_uri
16-
from .utils import track_response_code, use_slack_v2
17+
from sentry.shared_integrations.exceptions import ApiError, IntegrationError
18+
19+
from .client import SlackClient
20+
from .utils import logger, use_slack_v2
1721

1822
DESCRIPTION = """
1923
Connect your Sentry organization to one or more Slack workspaces, and start
@@ -120,15 +124,12 @@ def get_pipeline_views(self):
120124
def get_team_info(self, access_token):
121125
payload = {"token": access_token}
122126

123-
session = http.build_session()
124-
resp = session.get("https://slack.com/api/team.info", params=payload)
125-
resp.raise_for_status()
126-
status_code = resp.status_code
127-
resp = resp.json()
128-
# TODO: track_response_code won't hit if we have an error status code
129-
track_response_code(status_code, resp.get("ok"))
130-
131-
# TODO: check for resp["ok"]
127+
client = SlackClient()
128+
try:
129+
resp = client.get("/team.info", params=payload)
130+
except ApiError as e:
131+
logger.error("slack.team-info.response-error", extra={"error": six.text_type(e)})
132+
raise IntegrationError("Could not retrieve Slack team information.")
132133

133134
return resp["team"]
134135

src/sentry/integrations/slack/link_identity.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
from __future__ import absolute_import, print_function
22

3+
import six
4+
35
from django.core.urlresolvers import reverse
46
from django.db import IntegrityError
57
from django.http import Http404
68
from django.utils import timezone
79
from django.views.decorators.cache import never_cache
810

9-
from sentry import http
1011
from sentry.models import Integration, Identity, IdentityProvider, IdentityStatus, Organization
1112
from sentry.utils.http import absolute_uri
1213
from sentry.utils.signing import sign, unsign
14+
from sentry.web.decorators import transaction_start
1315
from sentry.web.frontend.base import BaseView
1416
from sentry.web.helpers import render_to_response
17+
from sentry.shared_integrations.exceptions import ApiError
1518

16-
from .utils import logger, track_response_code
19+
from .client import SlackClient
20+
from .utils import logger
1721

1822

1923
def build_linking_url(integration, organization, slack_id, channel_id, response_url):
@@ -31,6 +35,7 @@ def build_linking_url(integration, organization, slack_id, channel_id, response_
3135

3236

3337
class SlackLinkIdentityView(BaseView):
38+
@transaction_start("SlackLinkIdentityView")
3439
@never_cache
3540
def handle(self, request, signed_params):
3641
params = unsign(signed_params.encode("ascii", errors="ignore"))
@@ -82,19 +87,18 @@ def handle(self, request, signed_params):
8287
"text": "Your Slack identity has been linked to your Sentry account. You're good to go!",
8388
}
8489

85-
session = http.build_session()
86-
req = session.post(params["response_url"], json=payload)
87-
status_code = req.status_code
88-
resp = req.json()
89-
90-
# If the user took their time to link their slack account, we may no
91-
# longer be able to respond, and we're not guaranteed able to post into
92-
# the channel. Ignore Expired url errors.
93-
#
94-
# XXX(epurkhiser): Yes the error string has a space in it.
95-
if not resp.get("ok") and resp.get("error") != "Expired url":
96-
logger.error("slack.link-notify.response-error", extra={"response": resp})
97-
track_response_code(status_code, resp.get("ok"))
90+
client = SlackClient()
91+
try:
92+
client.post(params["response_url"], data=payload, json=True)
93+
except ApiError as e:
94+
message = six.text_type(e)
95+
# If the user took their time to link their slack account, we may no
96+
# longer be able to respond, and we're not guaranteed able to post into
97+
# the channel. Ignore Expired url errors.
98+
#
99+
# XXX(epurkhiser): Yes the error string has a space in it.
100+
if message != "Expired url":
101+
logger.error("slack.link-notify.response-error", extra={"error": message})
98102

99103
return render_to_response(
100104
"sentry/slack-linked.html",

src/sentry/integrations/slack/notify_action.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from __future__ import absolute_import
22

3+
import six
4+
35
from django import forms
46
from django.utils.translation import ugettext_lazy as _
57

6-
from sentry import http
78
from sentry.rules.actions.base import EventAction
89
from sentry.utils import metrics, json
910
from sentry.models import Integration
11+
from sentry.shared_integrations.exceptions import ApiError
1012

11-
from .utils import build_group_attachment, get_channel_id, strip_channel_name, track_response_code
13+
from .client import SlackClient
14+
from .utils import build_group_attachment, get_channel_id, strip_channel_name
1215

1316

1417
class SlackNotifyServiceForm(forms.Form):
@@ -112,19 +115,17 @@ def send_notification(event, futures):
112115
"attachments": json.dumps([attachment]),
113116
}
114117

115-
session = http.build_session()
116-
resp = session.post("https://slack.com/api/chat.postMessage", data=payload, timeout=5)
117-
status_code = resp.status_code
118-
response = resp.json()
119-
track_response_code(status_code, response.get("ok"))
120-
resp.raise_for_status()
121-
if not response.get("ok"):
118+
client = SlackClient()
119+
try:
120+
client.post("/chat.postMessage", data=payload, timeout=5)
121+
except ApiError as e:
122122
self.logger.info(
123123
"rule.fail.slack_post",
124124
extra={
125-
"error": response.get("error"),
125+
"error": six.text_type(e),
126126
"project_id": event.project_id,
127127
"event_id": event.event_id,
128+
"channel_name": self.get_option("channel"),
128129
},
129130
)
130131

0 commit comments

Comments
 (0)