Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(notifications): org request notification #29362

Merged
merged 16 commits into from
Oct 22, 2021
Merged

Conversation

scefali
Copy link
Contributor

@scefali scefali commented Oct 15, 2021

This PR creates a OrganizationRequestNotification class which is purposely built to handle various organization level requests (request to install integration, request to invite member, request to upgrade, etc). In making this more OOO friendly, I have added a class property called SlackMessageBuilderClass which defines which Slack builder a notification should be used. I moved some logic to the notification class to make sub-classing easier.

No actual notification classes are using OrganizationRequestNotification yet. That will come in future PRs.

Note that this PR is just a starting place. We should do more refactoring as a follow-up to make it cleaner and less hacky.

self.organization = organization

@property
def SlackMessageBuilderClass(self) -> Type["SlackNotificationsMessageBuilder"]:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hacking around an import problem. Maybe there is a better way we can have a specific Slack message builder be associated with a particular notification class.

) -> None:
super().__init__(notification, context, recipient)
# TODO: use generics here to do this
self.notification: ProjectNotification = notification
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there is a better way to do this but I'm not sure

class MessageAction:
label: str
url: str
style: Optional[str]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this style string an enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgaeta I did it as a literal, I think that works too, yes?

extra.update({"activity": self.activity})
return extra

def get_headers(self) -> Mapping[str, Any]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These headers are implementation details of the sentry.mail module and should be moved there. You can make X-Sentry-Project optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgaeta So you're saying remove the get_headers method from the class and just have everything handled in the existing get_headers method as already exists? What does the X-SMTPAPI header do?

Copy link
Contributor

@mgaeta mgaeta Oct 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes use get_headers() from sentry.mail.notifications.py. I'm not sure what the headers are for, they're from the legacy MailAdapter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgaeta Ok, we haven't been using any headers in the past for growth email notifications and since we don't know what the X-SMTPAPI does, I don't see why we'd need to add it now. How then should my notification class specify that it doesn't need headers if it can't subclass a get_headers function?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.sendgrid.com/for-developers/sending-email/getting-started-smtp
You should just use the X-SMTPAPI header with the category parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, that makes sense. thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgaeta switched back to using get_headers in sentry.mail.notifications

}
def build_subject_prefix(project: "Project") -> str:
key = "mail:subject_prefix"
return str(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to get build_subject_prefix() and generate_signed_link() to work with types you should avoid casting. Our best alternative right now is to explicitly annotate like:

# Explicitly typing to satisfy mypy.
subject_prefix: str = ProjectOption.objects.get_value(project, key) or options.get("mail.subject-prefix")
return subject_prefix

Also adding this file to mypy.ini will automatically send the PR to the python-typing group. They'll probably require that you move typing to a separate PR.

Copy link
Contributor Author

@scefali scefali Oct 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgaeta got it, I'll remove it from mypy.ini. I'll leave the function alone aside from removing the mail_option_key argument since it's never being applied.

for provider, recipients in participants_by_provider.items():
notify(provider, self, recipients, self.get_context())

def get_member(self, user: "User") -> "OrganizationMember":
Copy link
Contributor

@mgaeta mgaeta Oct 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make more sense to populate the member_by_user_id with a single query in case there are N admins for an organization.

recipients_cache: Optional[Set[Union["Team", "User"]]] = None
member_by_user_id: MutableMapping[int, OrganizationMember] = {}

def determine_recipients(self) -> Iterable[Union["Team", "User"]]:
    if recipients_cache:
	return recipients_cache
    recipients_cache = ...
    users = {recipient for recipient in recipients if isinstance(recipients, User)} 
    organization_memberships = OrganizationMember.objects.get(user__in=users, organization=self.organization) 
    self.member_by_user_id = {
     organization_membership.user_id: organization_membership
     for organization_membership in organization_memberships 
    }
    return recipients_cache

def get_member(self, user: "User") -> "OrganizationMember":
    return member_by_user_id.get(user.id)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgaeta I can pre-populate the member cache in determine_recipients but I still might want to fall back to a query if my member isn't in the cache, though that probably shouldn't happen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah also maybe after we read NotificationSettings there are no members at all and we could save the query altogether.

f"sentry-account-email-unsubscribe-{key}",
referrer,
kwargs={f"{key}_id": resource_id},
return str(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got lost in a previous comment but you should avoid casting to satisfy mypy.

signed_link: str = generate_signed_link(
    user_id,
    f"sentry-account-email-unsubscribe-{key}",
    referrer,
    kwargs={f"{key}_id": resource_id},
)
return signed_link

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgaeta good catch! Let me add that in

@scefali scefali merged commit 1ef36df into master Oct 22, 2021
@scefali scefali deleted the ref/org-request-notification branch October 22, 2021 16:36
scefali pushed a commit that referenced this pull request Oct 22, 2021
@github-actions github-actions bot locked and limited conversation to collaborators Nov 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants