-
Notifications
You must be signed in to change notification settings - Fork 18
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
Use Pinpoint by default #2173
Use Pinpoint by default #2173
Changes from 56 commits
9740520
064e36d
b9d0d3a
0fa34c1
7e69706
b3d12e5
fe263c6
d18ce27
fceab9f
b8ceb91
1a6ba9e
6e3d339
ca2da8c
7ec198b
3db48e8
d2e8471
c8eb6c5
6c090ce
3f59f27
646b86d
bde0f68
12f539f
965fbff
26d4419
5710df2
28524bb
efa0df9
d1612c2
c08cc14
aed401f
e205bc1
41e7eaa
40e4c87
f09cec5
fde4812
040a361
da4006f
188d0b6
b4e2bd8
afec912
337c594
7fb51e1
6e7de9f
ea7f071
70e540c
5dcaf6b
5717361
727070f
76ab640
b6b427b
82f0726
54d5b40
f906489
7859b95
abefbac
635883b
fb8f395
2a80780
45bd22b
554ef3c
91aca6a
8447d6f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ def get_name(self): | |
return self.name | ||
|
||
@statsd(namespace="clients.sns") | ||
def send_sms(self, to, content, reference, multi=True, sender=None): | ||
def send_sms(self, to, content, reference, multi=True, sender=None, template_id=None): | ||
matched = False | ||
|
||
for match in phonenumbers.PhoneNumberMatcher(to, "US"): | ||
|
@@ -66,12 +66,9 @@ def send_sms(self, to, content, reference, multi=True, sender=None): | |
try: | ||
start_time = monotonic() | ||
response = client.publish(PhoneNumber=to, Message=content, MessageAttributes=attributes) | ||
except botocore.exceptions.ClientError as e: | ||
self.statsd_client.incr("clients.sns.error") | ||
raise str(e) | ||
except Exception as e: | ||
self.statsd_client.incr("clients.sns.error") | ||
raise str(e) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This stuff was redundant and also the raise didn't work! You can't raise a string, just some subclass of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pylint for the win! I feel bad we had that for that long in the code. The whole error management does look weird in retrospective, thank you for refactoring. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ha, I didn't find it with pylint but by accidentally triggering the error locally (I was trying to send to a USA number without setting the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay. I thought it might be pylint because the latter did raise it as a potential problem when I looked at this code locally in vscode. |
||
raise Exception(e) | ||
sastels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
finally: | ||
elapsed_time = monotonic() - start_time | ||
self.current_app.logger.info("AWS SNS request finished in {}".format(elapsed_time)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -267,6 +267,7 @@ class Config(object): | |
AWS_SES_SECRET_KEY = os.getenv("AWS_SES_SECRET_KEY") | ||
AWS_PINPOINT_REGION = os.getenv("AWS_PINPOINT_REGION", "us-west-2") | ||
AWS_PINPOINT_SC_POOL_ID = os.getenv("AWS_PINPOINT_SC_POOL_ID", None) | ||
AWS_PINPOINT_DEFAULT_POOL_ID = os.getenv("AWS_PINPOINT_DEFAULT_POOL_ID", None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we really want There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok reading the code further, it seems that a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah exactly. A |
||
AWS_PINPOINT_CONFIGURATION_SET_NAME = os.getenv("AWS_PINPOINT_CONFIGURATION_SET_NAME", "pinpoint-configuration") | ||
AWS_PINPOINT_SC_TEMPLATE_IDS = env.list("AWS_PINPOINT_SC_TEMPLATE_IDS", []) | ||
AWS_US_TOLL_FREE_NUMBER = os.getenv("AWS_US_TOLL_FREE_NUMBER") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,10 @@ | |
import os | ||
import re | ||
from datetime import datetime | ||
from typing import Dict | ||
from typing import Any, Dict, Union | ||
|
||
from uuid import UUID | ||
|
||
import phonenumbers | ||
from flask import current_app | ||
from notifications_utils.recipients import ( | ||
validate_and_format_email_address, | ||
|
@@ -48,6 +49,7 @@ | |
NOTIFICATION_VIRUS_SCAN_FAILED, | ||
PINPOINT_PROVIDER, | ||
SMS_TYPE, | ||
SNS_PROVIDER, | ||
BounceRateStatus, | ||
Notification, | ||
Service, | ||
|
@@ -67,9 +69,9 @@ def send_sms_to_provider(notification): | |
provider = provider_to_use( | ||
SMS_TYPE, | ||
notification.id, | ||
notification.to, | ||
notification.international, | ||
notification.reply_to_text, | ||
template_id=notification.template_id, | ||
) | ||
|
||
template_dict = dao_get_template_by_id(notification.template_id, notification.template_version).__dict__ | ||
|
@@ -105,6 +107,7 @@ def send_sms_to_provider(notification): | |
content=str(template), | ||
reference=str(notification.id), | ||
sender=notification.reply_to_text, | ||
template_id=notification.template_id, | ||
) | ||
except Exception as e: | ||
notification.billable_units = template.fragment_count | ||
|
@@ -336,16 +339,55 @@ def update_notification_to_sending(notification, provider): | |
dao_update_notification(notification) | ||
|
||
|
||
def provider_to_use(notification_type, notification_id, international=False, sender=None, template_id=None): | ||
# Temporary redirect setup for template IDs that are meant for the short code usage. | ||
if notification_type == SMS_TYPE and template_id is not None and str(template_id) in Config.AWS_PINPOINT_SC_TEMPLATE_IDS: | ||
return clients.get_client_by_name_and_type("pinpoint", SMS_TYPE) | ||
def provider_to_use( | ||
notification_type: str, | ||
notification_id: UUID, | ||
to: Union[str, None] = None, | ||
sastels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
international: bool = False, | ||
sender: Union[str, None] = None, | ||
sastels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) -> Any: | ||
""" | ||
Get the provider to use for sending the notification. | ||
SMS that are being sent with a dedicated number or to a US number should not use Pinpoint. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the documentation and typing annotations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I figured I had added enough new logic to this function that some documentation was useful 😬 |
||
|
||
Args: | ||
notification_type (str): SMS or EMAIL. | ||
notification_id (UUID): id of notification. Just used for logging. | ||
to (str, optional): recipient. Defaults to None. | ||
international (bool, optional): Recipient is international. Defaults to False. | ||
sender (str, optional): reply_to_text to use. Defaults to None. | ||
|
||
Raises: | ||
Exception: No active providers. | ||
|
||
active_providers_in_order = [ | ||
p | ||
for p in get_provider_details_by_notification_type(notification_type, international) | ||
if p.active and p.identifier != PINPOINT_PROVIDER | ||
] | ||
Returns: | ||
provider: Provider to use to send the notification. | ||
""" | ||
|
||
has_dedicated_number = sender is not None and sender.startswith("+1") | ||
sending_to_us_number = False | ||
if to is not None: | ||
match = next(iter(phonenumbers.PhoneNumberMatcher(to, "US")), None) | ||
if match and phonenumbers.region_code_for_number(match.number) == "US": | ||
sending_to_us_number = True | ||
|
||
if ( | ||
has_dedicated_number | ||
or sending_to_us_number | ||
or current_app.config["AWS_PINPOINT_SC_POOL_ID"] is None | ||
or current_app.config["AWS_PINPOINT_DEFAULT_POOL_ID"] is None | ||
): | ||
active_providers_in_order = [ | ||
p | ||
for p in get_provider_details_by_notification_type(notification_type, international) | ||
if p.active and p.identifier != PINPOINT_PROVIDER | ||
] | ||
else: | ||
active_providers_in_order = [ | ||
p | ||
for p in get_provider_details_by_notification_type(notification_type, international) | ||
if p.active and p.identifier != SNS_PROVIDER | ||
] | ||
|
||
if not active_providers_in_order: | ||
current_app.logger.error("{} {} failed as no active providers".format(notification_type, notification_id)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
""" | ||
|
||
Revision ID: 0450_enable_pinpoint_provider | ||
Revises: 0449_update_magic_link_auth | ||
Create Date: 2021-01-08 09:03:00 .214680 | ||
|
||
""" | ||
from alembic import op | ||
|
||
revision = "0450_enable_pinpoint_provider" | ||
down_revision = "0449_update_magic_link_auth" | ||
|
||
|
||
def upgrade(): | ||
op.execute("UPDATE provider_details set active=true where identifier in ('pinpoint');") | ||
|
||
|
||
def downgrade(): | ||
op.execute("UPDATE provider_details set active=false where identifier in ('pinpoint');") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import pytest | ||
|
||
from app import aws_pinpoint_client | ||
from tests.conftest import set_config_values | ||
|
||
|
||
@pytest.mark.serial | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's some sort of race condition in our tests that I couldn't figure out :/ If these two tests are not run serially then something resets the config variables that they change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be a great topic for a code review session. It seems we have a misunderstanding on our some of our test concurrency is setup and fails when they should be successful. We can put our mind to it and try to brainstorm on this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I gave some more thoughts to this and one thing I wish we could do better is OOP in our code base. We barely have much and this could be our focus on code reviews session. We rely a lot on environment variables which is not a good practice in general, as the code relies on environment rather than configuration of the logic to run. Encapsulating the pool config in an object would make this easier probably to run instead of relying on environments. Do you know what I mean here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do pull the env vars out of the env and put into the In this case, though, in the test I made serial the app wasn't using the values of |
||
def test_send_sms_sends_to_default_pool(notify_api, mocker, sample_template): | ||
boto_mock = mocker.patch.object(aws_pinpoint_client, "_client", create=True) | ||
mocker.patch.object(aws_pinpoint_client, "statsd_client", create=True) | ||
to = "6135555555" | ||
content = "foo" | ||
reference = "ref" | ||
|
||
with set_config_values( | ||
notify_api, | ||
{ | ||
"AWS_PINPOINT_SC_POOL_ID": "sc_pool_id", | ||
"AWS_PINPOINT_DEFAULT_POOL_ID": "default_pool_id", | ||
"AWS_PINPOINT_CONFIGURATION_SET_NAME": "config_set_name", | ||
"AWS_PINPOINT_SC_TEMPLATE_IDS": [], | ||
}, | ||
): | ||
aws_pinpoint_client.send_sms(to, content, reference=reference, template_id=sample_template.id) | ||
|
||
boto_mock.send_text_message.assert_called_once_with( | ||
DestinationPhoneNumber="+16135555555", | ||
OriginationIdentity="default_pool_id", | ||
MessageBody=content, | ||
MessageType="TRANSACTIONAL", | ||
ConfigurationSetName="config_set_name", | ||
) | ||
|
||
|
||
@pytest.mark.serial | ||
def test_send_sms_sends_to_shortcode_pool(notify_api, mocker, sample_template): | ||
boto_mock = mocker.patch.object(aws_pinpoint_client, "_client", create=True) | ||
mocker.patch.object(aws_pinpoint_client, "statsd_client", create=True) | ||
to = "6135555555" | ||
content = "foo" | ||
reference = "ref" | ||
|
||
with set_config_values( | ||
notify_api, | ||
{ | ||
"AWS_PINPOINT_SC_POOL_ID": "sc_pool_id", | ||
"AWS_PINPOINT_DEFAULT_POOL_ID": "default_pool_id", | ||
"AWS_PINPOINT_CONFIGURATION_SET_NAME": "config_set_name", | ||
"AWS_PINPOINT_SC_TEMPLATE_IDS": [str(sample_template.id)], | ||
}, | ||
): | ||
aws_pinpoint_client.send_sms(to, content, reference=reference, template_id=sample_template.id) | ||
|
||
boto_mock.send_text_message.assert_called_once_with( | ||
DestinationPhoneNumber="+16135555555", | ||
OriginationIdentity="sc_pool_id", | ||
MessageBody=content, | ||
MessageType="TRANSACTIONAL", | ||
ConfigurationSetName="config_set_name", | ||
) | ||
|
||
|
||
def test_send_sms_returns_raises_error_if_there_is_no_valid_number_is_found(notify_api, mocker): | ||
mocker.patch.object(aws_pinpoint_client, "_client", create=True) | ||
mocker.patch.object(aws_pinpoint_client, "statsd_client", create=True) | ||
|
||
to = "" | ||
content = reference = "foo" | ||
|
||
with pytest.raises(ValueError) as excinfo: | ||
aws_pinpoint_client.send_sms(to, content, reference) | ||
|
||
assert "No valid numbers found for SMS delivery" in str(excinfo.value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have a warning alert in TF specifically for this one? 🤔 I guess it might fall into celery errors but these might be generic enough that we don't always check on these? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. possibly, though this is a bit of a strange test - testing that if the notification has an invalid phone number in it then we get a log. Agree though that this is a strange thing that we'd like surfaced immediately though. Basically it means that we screwed up the phone number and AWS doesn't recognize it. So, for example, we start sending texts to a new country and our number formatter messes up. Yeah, I'll add a warning for these 👍 |
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.
What happens if the pool_id is
None
? Is a randomOriginationIdentity
would be used then?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.
if the
OriginationIdentity
isNone
in the boto call then the call returns an errorBut as you mention below, we catch that case in our code and fall back to SNS.