Esempio n. 1
0
 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"
Esempio n. 2
0
    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)
Esempio n. 3
0
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
Esempio n. 4
0
        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"),
                    },
                )
Esempio n. 5
0
 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"
Esempio n. 6
0
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)
Esempio n. 7
0
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"],
    }
Esempio n. 8
0
        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"),
                    },
                )
Esempio n. 9
0
    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",
        }
Esempio n. 10
0
 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"
Esempio n. 11
0
 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"] == []
Esempio n. 12
0
    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)