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

Create a wrapper around the Mailjet API #57580

Merged
merged 11 commits into from Apr 11, 2024

Conversation

bethanyaconnor
Copy link
Contributor

@bethanyaconnor bethanyaconnor commented Mar 27, 2024

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.

@bethanyaconnor bethanyaconnor marked this pull request as ready for review March 27, 2024 20:01
@bethanyaconnor bethanyaconnor requested review from cat5inthecradle and a team March 27, 2024 20:02
Copy link
Contributor

@hannahbergam hannahbergam left a 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!

SECRET_KEY = CDO.try(:mailjet_secret_key).freeze

def self.enabled?
return false unless DCDO.get('use_mailjet', false)
Copy link
Contributor

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

Copy link
Contributor

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?

Suggested change
return false unless DCDO.get('use_mailjet', false)
return DCDO.get('use_mailjet', false)

Copy link
Contributor Author

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

Copy link
Contributor Author

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?

def self.create_contact_and_send_welcome_email(user)
return unless enabled?

return if user&.id.nil?
Copy link
Contributor

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 Show resolved Hide resolved

# 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉

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.
Copy link
Contributor

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?

Copy link
Contributor Author

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!

Copy link
Contributor

@cat5inthecradle cat5inthecradle left a 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

Comment on lines 43 to 82
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
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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)

Copy link
Contributor Author

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Comment on lines 16 to 25
def self.subaccount
case Rails.env
when 'production'
'production'
when 'staging'
'staging'
else
'development'
end
end
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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)

Copy link
Contributor Author

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

@bethanyaconnor bethanyaconnor merged commit c0a6861 into staging Apr 11, 2024
2 checks passed
@bethanyaconnor bethanyaconnor deleted the bethany/lifecycle-marketing/mailjet-wrapper branch April 11, 2024 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants