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
Create a wrapper around the Mailjet API #57580
Create a wrapper around the Mailjet API #57580
Conversation
…marketing/mailjet-wrapper
…marketing/mailjet-wrapper
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a few questions- thanks for adding the tests!
lib/cdo/mailjet.rb
Outdated
SECRET_KEY = CDO.try(:mailjet_secret_key).freeze | ||
|
||
def self.enabled? | ||
return false unless DCDO.get('use_mailjet', false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I always have a hard time reading 'unless', but I'm surprised ust_mailjet would be false here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels super unintuitive and requires a few mental jumps. Why can't we just use the much simpler form?
return false unless DCDO.get('use_mailjet', false) | |
return DCDO.get('use_mailjet', false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Combined this line with the one below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And used present?
over negating nil?
lib/cdo/mailjet.rb
Outdated
def self.create_contact_and_send_welcome_email(user) | ||
return unless enabled? | ||
|
||
return if user&.id.nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we switch this one to an 'unless' to match the other two for readability?
lib/cdo/mailjet.rb
Outdated
|
||
# If a contact already exists, Mailjet will raise an error. | ||
# This shouldn't happen as user emails should be unique, but | ||
# we don't want to block sign up if it does. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
lib/cdo/mailjet.rb
Outdated
end | ||
|
||
# Most likely, the above would fail if a contact already exists. | ||
# In that case, we want to update the contact with the sign up date. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this in the case that (for example), someone would opt in to receive the 'host an hour of code' email but not yet have an account?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oo true, I always forget about how many ways we get user emails. That seems like a likely pathway!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got some nits about the implementation and always like to see more exhaustive tests around non-happy paths.
At a high level I think that the API you're exposing from Cdo::MailJet makes sense, and gives us a good place to wire in error handling, metrics, etc
lib/cdo/mailjet.rb
Outdated
def self.create_contact(email, name, sign_up_date) | ||
return unless enabled? | ||
|
||
Mailjet.configure do |config| | ||
config.api_key = API_KEY | ||
config.secret_key = SECRET_KEY | ||
config.api_version = "v3" | ||
end | ||
|
||
# If a contact already exists, Mailjet will raise an error. | ||
# This shouldn't happen as user emails should be unique, but | ||
# we don't want to block sign up if it does. | ||
begin | ||
Mailjet::Contact.create( | ||
is_excluded_from_campaigns: true, | ||
email: email, | ||
name: name | ||
) | ||
rescue => exception | ||
Honeybadger.notify(exception) | ||
end | ||
|
||
# Most likely, the above would fail if a contact already exists. | ||
# In that case, we want to update the contact with the sign up date. | ||
# However, in the case of a different error, we want to notify Honeybadger, | ||
# but not block sign up. | ||
begin | ||
contact = Mailjet::Contactdata.find(email) | ||
contact.update_attributes( | ||
data: [ | ||
{ | ||
name: 'sign_up_date', | ||
value: sign_up_date | ||
} | ||
] | ||
) | ||
rescue => exception | ||
Honeybadger.notify(exception) | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now this function always makes three API calls. Can we reduce that to one or two calls by rearranging it?
- FIND contact by email
- if found, UPDATE the sign_up_date if necessary
- else CREATE with sign_up_date attributes
I think that would also make it more straightforward to add test cases for the various code paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, this function is squashing all errors. That seems undesireable and I can think of a few scenarios where you definitely don't want to be calling send_template_email
if create_contact
fails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CREATE with sign_up_date attributes
This always requires two API calls because properties have to be updated through a separate API.
I think you've got a good point about finding the contact first then branching though -- I'll update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that's frustrating. In that case perhaps the create doing double duty as an existence check is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored and it's much cleaner! I ended up removing the error squashing in general, but I'm very open to discussion there. In particular, moving the call to find the contact up means that the primary error case disappeared, so I'm less attached to squashing the errors. There's a part of me that's considering adding them back, however, in case MailJet is down or something. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depends if we're going to do anything about it. For a general wrapper, I think it makes sense to raise an exception if create_contact
fails to create a contact. Catching that exception is the responsibility of the caller, and it can decide whether to retry, fail, or move on. Unless there's a specific exception category that this wrapper should be responsible for resolving (for example, I've worked with API's that have rate limits that were resolved by trying again one more time right now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense! And thinking through it, I don't want to add any exception handling here. When I hook this up into the actual user sign up flow, I might, but that is coming in a future PR.
Name: to_name | ||
} | ||
], | ||
TemplateID: template_id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would there be value in enforcing that the template_id be found in MailJetConstants
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean to push the fetching of the template ID down to this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just noticing that there's nothing to force future developers to use to use the template listing instead of hardcoding template ID's elsewhere in the codebase. Once we have examples of using the wrapper, the convention might be more clear though.
lib/cdo/mailjet.rb
Outdated
def self.subaccount | ||
case Rails.env | ||
when 'production' | ||
'production' | ||
when 'staging' | ||
'staging' | ||
else | ||
'development' | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was re-reading the tech plan and was reminded that we're explicitly planning on NOT connecting to mailjet for Test and Drone. Is there a good place to make that intention clear in the code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah -- I could add a check in enabled?
. Let me do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, sorry, I meant clear to future developers wondering why production and staging are getting values but not anything else (because we don't have a standard convention or clear guidance about the different environments)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh gotcha! Yeah I can add something here as well
This PR creates a wrapper around the
mailjet
SDK. It also creates the method that will send the welcome email and create a contact.I decided to include a constants file that lists the template ids (ids from the mailjet platform), as well as some general metadata about each email (such as who it is from). I figured this would be extensible if we ever moved workshop emails to this platform as well, which don't come from Hadi. In all likelihood, I'll update these constants again before full launch.
See the tech plan for details.