Ejemplo n.º 1
0
def get_context(
    notification: BaseNotification,
    recipient: Team | User,
    shared_context: Mapping[str, Any],
    extra_context: Mapping[str, Any],
) -> Mapping[str, Any]:
    """
    Compose the various levels of context and add email-specific fields. The
    generic HTML/text templates only render the unsubscribe link if one is
    present in the context, so don't automatically add it to every message.
    """
    context = {
        **shared_context,
        **notification.get_recipient_context(recipient, extra_context),
    }
    # TODO(mgaeta): The unsubscribe system relies on `user_id` so it doesn't
    #  work with Teams. We should add the `actor_id` to the signed link.
    unsubscribe_key = notification.get_unsubscribe_key()
    if isinstance(recipient, User) and unsubscribe_key:
        key, resource_id, referrer = unsubscribe_key
        context.update({
            "unsubscribe_link":
            get_unsubscribe_link(recipient.id, resource_id, key, referrer)
        })

    return context
Ejemplo n.º 2
0
def send_notification_as_email(
    notification: BaseNotification,
    users: Set[User],
    shared_context: Mapping[str, Any],
    extra_context_by_user_id: Optional[Mapping[int, Mapping[str, Any]]],
) -> None:
    headers = get_headers(notification)

    for user in users:
        extra_context = (extra_context_by_user_id or {}).get(user.id, {})
        log_message(notification, user)
        context = get_context(notification, user, shared_context,
                              extra_context)
        subject = get_subject_with_prefix(notification, context=context)
        msg = MessageBuilder(
            subject=subject,
            context=context,
            template=notification.get_template(),
            html_template=notification.get_html_template(),
            headers=headers,
            reference=notification.get_reference(),
            reply_reference=notification.get_reply_reference(),
            type=notification.get_type(),
        )
        msg.add_users([user.id], project=notification.project)
        msg.send_async()
Ejemplo n.º 3
0
def send_notification_as_email(
    notification: BaseNotification,
    recipients: Iterable[Team | User],
    shared_context: Mapping[str, Any],
    extra_context_by_actor_id: Mapping[int, Mapping[str, Any]] | None,
) -> None:
    for recipient in recipients:
        with sentry_sdk.start_span(op="notification.send_email",
                                   description="one_recipient"):
            if isinstance(recipient, Team):
                # TODO(mgaeta): MessageBuilder only works with Users so filter out Teams for now.
                continue
            log_message(notification, recipient)

            with sentry_sdk.start_span(op="notification.send_email",
                                       description="build_message"):
                msg = MessageBuilder(
                    **get_builder_args(notification, recipient, shared_context,
                                       extra_context_by_actor_id))

            with sentry_sdk.start_span(op="notification.send_email",
                                       description="send_message"):
                # TODO: find better way of handling this
                add_users_kwargs = {}
                if isinstance(notification, ProjectNotification):
                    add_users_kwargs["project"] = notification.project
                msg.add_users([recipient.id], **add_users_kwargs)
                msg.send_async()
            notification.record_notification_sent(recipient,
                                                  ExternalProviders.EMAIL)
Ejemplo n.º 4
0
def send_notification_as_email(
    notification: BaseNotification,
    recipients: Iterable[Union["Team", "User"]],
    shared_context: Mapping[str, Any],
    extra_context_by_user_id: Optional[Mapping[int, Mapping[str, Any]]],
) -> None:
    headers = get_headers(notification)

    for recipient in recipients:
        if isinstance(recipient, Team):
            # TODO(mgaeta): MessageBuilder only works with Users so filter out Teams for now.
            continue
        extra_context = (extra_context_by_user_id or {}).get(recipient.id, {})
        log_message(notification, recipient)
        context = get_context(notification, recipient, shared_context,
                              extra_context)
        subject = get_subject_with_prefix(notification, context=context)
        msg = MessageBuilder(
            subject=subject,
            context=context,
            template=notification.get_template(),
            html_template=notification.get_html_template(),
            headers=headers,
            reference=notification.get_reference(),
            reply_reference=notification.get_reply_reference(),
            type=notification.get_type(),
        )
        msg.add_users([recipient.id], project=notification.project)
        msg.send_async()
Ejemplo n.º 5
0
def get_builder_args_from_context(
        notification: BaseNotification,
        context: Mapping[str, Any]) -> MutableMapping[str, Any]:
    output = {
        "subject": get_subject_with_prefix(notification, context),
        "context": context,
        "template": notification.get_template(),
        "html_template": notification.get_html_template(),
        "headers": get_headers(notification),
        "reference": notification.get_reference(),
        "reply_reference": notification.get_reply_reference(),
        "type": notification.get_type(),
    }
    # add in optinal fields
    from_email = notification.from_email
    if from_email:
        output["from_email"] = from_email
    return output
Ejemplo n.º 6
0
def _notify_recipient(
    notification: BaseNotification,
    recipient: Team | User,
    attachments: List[SlackAttachment],
    channel: str,
    integration: Integration,
) -> None:
    with sentry_sdk.start_span(op="notification.send_slack", description="notify_recipient"):
        # Make a local copy to which we can append.
        local_attachments = copy(attachments)

        token: str = integration.metadata["access_token"]

        # Add optional billing related attachment.
        additional_attachment = get_additional_attachment(integration, notification.organization)
        if additional_attachment:
            local_attachments.append(additional_attachment)

        # unfurl_links and unfurl_media are needed to preserve the intended message format
        # and prevent the app from replying with help text to the unfurl
        payload = {
            "token": token,
            "channel": channel,
            "link_names": 1,
            "unfurl_links": False,
            "unfurl_media": False,
            "text": notification.get_notification_title(),
            "attachments": json.dumps(local_attachments),
        }

        log_params = {
            "notification": notification,
            "recipient": recipient.id,
            "channel_id": channel,
        }
        post_message.apply_async(
            kwargs={
                "payload": payload,
                "log_error_message": "notification.fail.slack_post",
                "log_params": log_params,
            }
        )
    # recording data outside of span
    notification.record_notification_sent(recipient, ExternalProviders.SLACK)
Ejemplo n.º 7
0
def get_context(
    notification: BaseNotification,
    recipient: Union[User, Team],
    shared_context: Mapping[str, Any],
    extra_context: Mapping[str, Any],
) -> Mapping[str, Any]:
    """Compose the various levels of context and add Slack-specific fields."""
    return {
        **shared_context,
        **notification.get_user_context(recipient, extra_context),
    }
Ejemplo n.º 8
0
def get_builder_args(
    notification: BaseNotification,
    recipient: User,
    shared_context: Mapping[str, Any] | None = None,
    extra_context_by_actor_id: Mapping[int, Mapping[str, Any]] | None = None,
) -> Mapping[str, Any]:
    # TODO: move context logic to single notification class method
    extra_context = (extra_context_by_actor_id
                     or {}).get(recipient.actor_id, {})
    context = get_context(notification, recipient, shared_context or {},
                          extra_context)
    return {
        "subject": get_subject_with_prefix(notification, context),
        "context": context,
        "template": notification.get_template(),
        "html_template": notification.get_html_template(),
        "headers": get_headers(notification),
        "reference": notification.get_reference(),
        "reply_reference": notification.get_reply_reference(),
        "type": notification.get_type(),
    }
Ejemplo n.º 9
0
def get_context(
    notification: BaseNotification,
    user: User,
    shared_context: Mapping[str, Any],
    extra_context: Mapping[str, Any],
) -> Mapping[str, Any]:
    """
    Compose the various levels of context and add email-specific fields. The
    generic HTML/text templates only render the unsubscribe link if one is
    present in the context, so don't automatically add it to every message.
    """
    context = {
        **shared_context,
        **notification.get_user_context(user, extra_context),
    }
    if notification.get_unsubscribe_key():
        key, resource_id, referrer = notification.get_unsubscribe_key()
        context.update({
            "unsubscribe_link":
            get_unsubscribe_link(user.id, resource_id, key, referrer)
        })

    return context
Ejemplo n.º 10
0
def get_headers(notification: BaseNotification) -> Mapping[str, Any]:
    headers = {
        "X-Sentry-Project": notification.project.slug,
        "X-SMTPAPI": json.dumps({"category": notification.get_category()}),
    }

    group = getattr(notification, "group", None)
    if group:
        headers.update({
            "X-Sentry-Logger": group.logger,
            "X-Sentry-Logger-Level": group.get_level_display(),
            "X-Sentry-Reply-To": group_id_to_email(group.id),
        })

    return headers
Ejemplo n.º 11
0
def log_message(notification: BaseNotification,
                recipient: Team | User) -> None:
    extra = notification.get_log_params(recipient)
    logger.info("mail.adapter.notify.mail_user", extra={**extra})
Ejemplo n.º 12
0
def send_notification_as_slack(
    notification: BaseNotification,
    recipients: Union[Set[User], Set[Team]],
    shared_context: Mapping[str, Any],
    extra_context_by_user_id: Optional[Mapping[int, Mapping[str, Any]]],
) -> None:
    """Send an "activity" or "alert rule" notification to a Slack user or team."""
    client = SlackClient()
    data = get_channel_and_token_by_recipient(notification.organization,
                                              recipients)

    for recipient, tokens_by_channel in data.items():
        is_multiple = True if len([token for token in tokens_by_channel
                                   ]) > 1 else False
        if is_multiple:
            logger.info(
                "notification.multiple.slack_post",
                extra={
                    "notification": notification,
                    "recipient": recipient.id,
                },
            )
        extra_context = (extra_context_by_user_id or {}).get(recipient.id, {})
        context = get_context(notification, recipient, shared_context,
                              extra_context)
        attachment = [
            build_notification_attachment(notification, context, recipient)
        ]
        for channel, token in tokens_by_channel.items():
            # unfurl_links and unfurl_media are needed to preserve the intended message format
            # and prevent the app from replying with help text to the unfurl
            payload = {
                "token": token,
                "channel": channel,
                "link_names": 1,
                "unfurl_links": False,
                "unfurl_media": False,
                "text": notification.get_notification_title(),
                "attachments": json.dumps(attachment),
            }
            try:
                client.post("/chat.postMessage", data=payload, timeout=5)
            except ApiError as e:
                logger.info(
                    "notification.fail.slack_post",
                    extra={
                        "error": str(e),
                        "notification": notification,
                        "recipient": recipient.id,
                        "channel_id": channel,
                        "is_multiple": is_multiple,
                    },
                )
            analytics.record(
                "integrations.slack.notification_sent",
                organization_id=notification.organization.id,
                project_id=notification.project.id,
                category=notification.get_category(),
                actor_id=recipient.actor_id,
            )

    key = get_key(notification)
    metrics.incr(
        f"{key}.notifications.sent",
        instance=f"slack.{key}.notification",
        skip_internal=False,
    )
Ejemplo n.º 13
0
def log_message(notification: BaseNotification,
                recipient: Team | User) -> None:
    extra = notification.get_log_params(recipient)
    logger.info("mail.adapter.notify.mail_user", extra={**extra})
    notification.record_notification_sent(recipient, ExternalProviders.EMAIL)
Ejemplo n.º 14
0
def send_notification_as_slack(
    notification: BaseNotification,
    recipients: Iterable[Team | User],
    shared_context: Mapping[str, Any],
    extra_context_by_actor_id: Mapping[int, Mapping[str, Any]] | None,
) -> None:
    """Send an "activity" or "alert rule" notification to a Slack user or team."""
    data = get_integrations_by_channel_by_recipient(notification.organization,
                                                    recipients)
    for recipient, channels_to_integrations in data.items():
        is_multiple = (True if len(
            [integration
             for integration in channels_to_integrations]) > 1 else False)
        if is_multiple:
            logger.info(
                "notification.multiple.slack_post",
                extra={
                    "notification": notification,
                    "recipient": recipient.id,
                },
            )
        extra_context = (extra_context_by_actor_id
                         or {}).get(recipient.actor_id, {})
        context = get_context(notification, recipient, shared_context,
                              extra_context)
        attachments = get_attachments(notification, recipient, context)

        for channel, integration in channels_to_integrations.items():
            # make a local copy for appending
            local_attachments = copy(attachments)
            token: str = integration.metadata["access_token"]
            # getsentry might add a billing related attachment
            additional_attachment = get_additional_attachment(
                integration, notification.organization)
            if additional_attachment:
                local_attachments.append(additional_attachment)

            # unfurl_links and unfurl_media are needed to preserve the intended message format
            # and prevent the app from replying with help text to the unfurl
            payload = {
                "token": token,
                "channel": channel,
                "link_names": 1,
                "unfurl_links": False,
                "unfurl_media": False,
                "text": notification.get_notification_title(),
                "attachments": json.dumps(local_attachments),
            }
            log_params = {
                "notification": notification,
                "recipient": recipient.id,
                "channel_id": channel,
                "is_multiple": is_multiple,
            }
            post_message.apply_async(
                kwargs={
                    "payload": payload,
                    "log_error_message": "notification.fail.slack_post",
                    "log_params": log_params,
                })
            notification.record_notification_sent(recipient,
                                                  ExternalProviders.SLACK)

    key = notification.metrics_key
    metrics.incr(
        f"{key}.notifications.sent",
        instance=f"slack.{key}.notification",
        skip_internal=False,
    )