def test_build_group_attachment_color_warning(self): warning_event = self.store_event(data={"level": "warning"}, project_id=self.project.id) assert build_group_attachment( warning_event.group)["color"] == "#FFC227" assert build_group_attachment(warning_event.group, warning_event)["color"] == "#FFC227"
def test_unfurl_issues(self): min_ago = iso_format(before_now(minutes=1)) event = self.store_event(data={ "fingerprint": ["group2"], "timestamp": min_ago }, project_id=self.project.id) group2 = event.group links = [ UnfurlableUrl( url= f"https://sentry.io/organizations/{self.organization.slug}/issues/{self.group.id}/", args={ "issue_id": self.group.id, "event_id": None }, ), UnfurlableUrl( url= f"https://sentry.io/organizations/{self.organization.slug}/issues/{group2.id}/{event.event_id}/", args={ "issue_id": group2.id, "event_id": event.event_id }, ), ] unfurls = link_handlers[LinkType.ISSUES].fn(self.request, self.integration, links) assert unfurls[links[0].url] == build_group_attachment(self.group) assert unfurls[links[1].url] == build_group_attachment( group2, event, link_to_event=True)
def unfurl_issues( request: HttpRequest, integration: Integration, links: List[UnfurlableUrl], user: Optional["User"] = None, ) -> UnfurledUrl: """ Returns a map of the attachments used in the response we send to Slack for a particular issue by the URL of the yet-unfurled links a user included in their Slack message. """ group_by_id = { g.id: g for g in Group.objects.filter( id__in={link.args["issue_id"] for link in links}, project__in=Project.objects.filter(organization__in=integration.organizations.all()), ) } if not group_by_id: return {} out = {} for link in links: issue_id = link.args["issue_id"] if issue_id in group_by_id: group = group_by_id[issue_id] # lookup the event by the id event_id = link.args["event_id"] event = eventstore.get_event_by_id(group.project_id, event_id) if event_id else None out[link.url] = build_group_attachment( group_by_id[issue_id], event=event, link_to_event=True ) return out
def send_notification(event: Event, futures: Sequence[RuleFuture]) -> None: rules = [f.rule for f in futures] attachments = [build_group_attachment(event.group, event=event, tags=tags, rules=rules)] # getsentry might add a billing related attachment additional_attachment = get_additional_attachment( integration, self.project.organization ) if additional_attachment: attachments.append(additional_attachment) payload = { "token": integration.metadata["access_token"], "channel": channel, "link_names": 1, "attachments": json.dumps(attachments), } client = SlackClient() try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: self.logger.info( "rule.fail.slack_post", extra={ "error": str(e), "project_id": event.project_id, "event_id": event.event_id, "channel_name": self.get_option("channel"), }, )
def test_build_group_attachment_color_unexpected_level_error_fallback( self): unexpected_level_event = self.store_event(data={"level": "trace"}, project_id=self.project.id, assert_no_errors=False) assert build_group_attachment( unexpected_level_event.group)["color"] == "#E03E2F"
def build_issue_notification_attachment( group: Group, event=None, tags: Mapping[str, str] = None, rules: List[Rule] = None, ): return build_group_attachment(group, event, tags, rules, issue_alert=True)
def build_notification_attachment( notification: BaseNotification, context: Mapping[str, Any]) -> Mapping[str, str]: if isinstance(notification, AlertRuleNotification): return build_group_attachment( notification.group, notification.event, context["tags"], notification.rules, issue_alert=True, ) footer = build_notification_footer(notification) return { "title": notification.get_title(), "text": context["text_description"], "mrkdwn_in": ["text"], "footer_icon": get_sentry_avatar_url(), "footer": footer, "color": LEVEL_TO_COLOR["info"], }
def send_notification(event, futures): rules = [f.rule for f in futures] attachments = [build_group_attachment(event.group, event=event, tags=tags, rules=rules)] payload = { "token": integration.metadata["access_token"], "channel": channel, "link_names": 1, "attachments": json.dumps(attachments), } client = SlackClient() try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: self.logger.info( "rule.fail.slack_post", extra={ "error": str(e), "project_id": event.project_id, "event_id": event.event_id, "channel_name": self.get_option("channel"), }, )
def test_build_group_attachment(self): self.user = self.create_user("*****@*****.**") self.org = self.create_organization(name="Rowdy Tiger", owner=None) self.team = self.create_team(organization=self.org, name="Mariachi Band") self.project = self.create_project( organization=self.org, teams=[self.team], name="Bengal-Elephant-Giraffe-Tree-House") self.create_member(user=self.user, organization=self.org, role="owner", teams=[self.team]) group = self.create_group(project=self.project) ts = group.last_seen assert build_group_attachment(group) == { "color": "#E03E2F", "text": "", "actions": [ { "name": "status", "text": "Resolve", "type": "button", "value": "resolved" }, { "text": "Ignore", "type": "button", "name": "status", "value": "ignored" }, { "option_groups": [ { "text": "Teams", "options": [{ "text": "#mariachi-band", "value": "team:" + str(self.team.id), }], }, { "text": "People", "options": [{ "text": "*****@*****.**", "value": "user:"******"text": "Select Assignee...", "selected_options": [None], "type": "select", "name": "assign", }, ], "mrkdwn_in": ["text"], "title": group.title, "fields": [], "footer": "BENGAL-ELEPHANT-GIRAFFE-TREE-HOUSE-1", "ts": to_timestamp(ts), "title_link": "http://testserver/organizations/rowdy-tiger/issues/" + str(group.id) + "/?referrer=slack", "callback_id": '{"issue":' + str(group.id) + "}", "fallback": f"[{self.project.slug}] {group.title}", "footer_icon": "http://testserver/_static/{version}/sentry/images/sentry-email-avatar.png", } event = self.store_event(data={}, project_id=self.project.id) ts = event.datetime assert build_group_attachment(group, event) == { "color": "#E03E2F", "text": "", "actions": [ { "name": "status", "text": "Resolve", "type": "button", "value": "resolved" }, { "text": "Ignore", "type": "button", "name": "status", "value": "ignored" }, { "option_groups": [ { "text": "Teams", "options": [{ "text": "#mariachi-band", "value": "team:" + str(self.team.id), }], }, { "text": "People", "options": [{ "text": "*****@*****.**", "value": "user:"******"text": "Select Assignee...", "selected_options": [None], "type": "select", "name": "assign", }, ], "mrkdwn_in": ["text"], "title": event.title, "fields": [], "footer": "BENGAL-ELEPHANT-GIRAFFE-TREE-HOUSE-1", "ts": to_timestamp(ts), "title_link": "http://testserver/organizations/rowdy-tiger/issues/" + str(group.id) + "/?referrer=slack", "callback_id": '{"issue":' + str(group.id) + "}", "fallback": f"[{self.project.slug}] {event.title}", "footer_icon": "http://testserver/_static/{version}/sentry/images/sentry-email-avatar.png", } assert build_group_attachment(group, event, link_to_event=True) == { "color": "#E03E2F", "text": "", "actions": [ { "name": "status", "text": "Resolve", "type": "button", "value": "resolved" }, { "text": "Ignore", "type": "button", "name": "status", "value": "ignored" }, { "option_groups": [ { "text": "Teams", "options": [{ "text": "#mariachi-band", "value": "team:" + str(self.team.id), }], }, { "text": "People", "options": [{ "text": "*****@*****.**", "value": "user:"******"text": "Select Assignee...", "selected_options": [None], "type": "select", "name": "assign", }, ], "mrkdwn_in": ["text"], "title": event.title, "fields": [], "footer": "BENGAL-ELEPHANT-GIRAFFE-TREE-HOUSE-1", "ts": to_timestamp(ts), "title_link": f"http://testserver/organizations/rowdy-tiger/issues/{group.id}/events/{event.event_id}/" + "?referrer=slack", "callback_id": '{"issue":' + str(group.id) + "}", "fallback": f"[{self.project.slug}] {event.title}", "footer_icon": "http://testserver/_static/{version}/sentry/images/sentry-email-avatar.png", }
def test_build_group_attachment_color_no_event_error_fallback(self): group_with_no_events = self.create_group(project=self.project) assert build_group_attachment( group_with_no_events)["color"] == "#E03E2F"
def test_build_group_attachment_issue_alert(self): issue_alert_group = self.create_group(project=self.project) assert build_group_attachment(issue_alert_group, issue_alert=True)["actions"] == []
def post(self, request: Request) -> Response: logging_data: Dict[str, str] = {} try: slack_request = SlackActionRequest(request) slack_request.validate() except SlackRequestError as e: return self.respond(status=e.status) data = slack_request.data # Actions list may be empty when receiving a dialog response action_list = data.get("actions", []) action_option = action_list and action_list[0].get("value", "") # if a user is just clicking our auto response in the messages tab we just return a 200 if action_option == "sentry_docs_link_clicked": return self.respond() channel_id = slack_request.channel_id user_id = slack_request.user_id integration = slack_request.integration response_url = data.get("response_url") if action_option in ["link", "ignore"]: analytics.record( "integrations.slack.chart_unfurl_action", organization_id=integration.organizations.all()[0].id, action=action_option, ) payload = {"delete_original": "true"} try: post(response_url, json=payload) except ApiError as e: logger.error("slack.action.response-error", extra={"error": str(e)}) return self.respond(status=403) return self.respond() logging_data["channel_id"] = channel_id logging_data["slack_user_id"] = user_id logging_data["response_url"] = response_url logging_data["integration_id"] = integration.id # Determine the issue group action is being taken on group_id = slack_request.callback_data["issue"] logging_data["group_id"] = group_id try: group = Group.objects.select_related("project__organization").get( project__in=Project.objects.filter( organization__in=integration.organizations.all()), id=group_id, ) except Group.DoesNotExist: logger.info("slack.action.invalid-issue", extra=logging_data) return self.respond(status=403) logging_data["organization_id"] = group.organization.id # Determine the acting user by slack identity try: idp = IdentityProvider.objects.get( type="slack", external_id=slack_request.team_id) except IdentityProvider.DoesNotExist: logger.error("slack.action.invalid-team-id", extra=logging_data) return self.respond(status=403) try: identity = Identity.objects.select_related("user").get( idp=idp, external_id=user_id) except Identity.DoesNotExist: associate_url = build_linking_url(integration, group.organization, user_id, channel_id, response_url) return self.respond({ "response_type": "ephemeral", "replace_original": False, "text": LINK_IDENTITY_MESSAGE.format(associate_url=associate_url), }) # Handle status dialog submission if slack_request.type == "dialog_submission" and "resolve_type" in data[ "submission"]: # Masquerade a status action action = { "name": "status", "value": data["submission"]["resolve_type"] } try: self.on_status(request, identity, group, action, data, integration) except client.ApiError as e: if e.status_code == 403: text = UNLINK_IDENTITY_MESSAGE.format( associate_url=build_unlinking_url( integration.id, user_id, channel_id, response_url), user_email=identity.user, org_name=group.organization.name, ) else: text = DEFAULT_ERROR_MESSAGE return self.api_error(e, "status_dialog", logging_data, text) group = Group.objects.get(id=group.id) attachment = build_group_attachment(group, identity=identity, actions=[action]) body = self.construct_reply( attachment, is_message=slack_request.callback_data["is_message"]) # use the original response_url to update the link attachment slack_client = SlackClient() try: slack_client.post( slack_request.callback_data["orig_response_url"], data=body, json=True) except ApiError as e: logger.error("slack.action.response-error", extra={"error": str(e)}) return self.respond() # Usually we'll want to respond with the updated attachment including # the list of actions taken. However, when opening a dialog we do not # have anything to update the message with and will use the # response_url later to update it. defer_attachment_update = False # Handle interaction actions action_type = None try: for action in action_list: action_type = action["name"] if action_type == "status": self.on_status(request, identity, group, action, data, integration) elif action_type == "assign": self.on_assign(request, identity, group, action) elif action_type == "resolve_dialog": self.open_resolve_dialog(data, group, integration) defer_attachment_update = True except client.ApiError as e: if e.status_code == 403: text = UNLINK_IDENTITY_MESSAGE.format( associate_url=build_unlinking_url(integration.id, user_id, channel_id, response_url), user_email=identity.user, org_name=group.organization.name, ) else: text = DEFAULT_ERROR_MESSAGE return self.api_error(e, action_type, logging_data, text) if defer_attachment_update: return self.respond() # Reload group as it may have been mutated by the action group = Group.objects.get(id=group.id) attachment = build_group_attachment(group, identity=identity, actions=action_list) body = self.construct_reply(attachment, is_message=self.is_message(data)) return self.respond(body)