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
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()
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)
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()
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
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)
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), }
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(), }
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
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
def log_message(notification: BaseNotification, recipient: Team | User) -> None: extra = notification.get_log_params(recipient) logger.info("mail.adapter.notify.mail_user", extra={**extra})
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, )
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)
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, )