Skip to content

Commit

Permalink
feat: check for external ids in batchs when creating aliases.
Browse files Browse the repository at this point in the history
ENT-8325 | Query `/users/export/ids` using batches of alias records
when checking for existing accounts/external ids in `create_braze_alias()`.
  • Loading branch information
iloveagent57 committed Jan 25, 2024
1 parent e46cd8f commit 9e7a0d9
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Change Log
Unreleased
~~~~~~~~~~

[0.2.0]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
feat: check for external ids in batchs when creating aliases.

[0.1.8]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
fix: always create an alias for existing profiles
Expand Down
9 changes: 2 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,9 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy

quality: ## check coding style with pycodestyle and pylint
touch tests/__init__.py
pylint braze tests test_utils manage.py *.py
pylint braze tests test_utils *.py
pycodestyle braze tests *.py
pydocstyle braze tests *.py
isort --check-only --diff --recursive tests test_utils braze *.py test_settings.py
python setup.py bdist_wheel
twine check dist/*
make selfcheck

isort --check-only --diff --recursive tests test_utils braze *.py

requirements: ## install development environment requirements
pip install -r requirements/pip.txt
Expand Down
2 changes: 1 addition & 1 deletion braze/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Python client for interacting with Braze APIs.
"""

__version__ = '0.1.8'
__version__ = '0.2.0'
47 changes: 46 additions & 1 deletion braze/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
"""
import datetime
import json
import logging
from collections import deque
from urllib.parse import urljoin

import requests

from braze.constants import (
GET_EXTERNAL_IDS_CHUNK_SIZE,
REQUEST_TYPE_GET,
REQUEST_TYPE_POST,
TRACK_USER_COMPONENT_CHUNK_SIZE,
Expand All @@ -29,6 +31,8 @@
BrazeUnauthorizedError,
)

logger = logging.getLogger(__name__)


class BrazeClient:
"""
Expand Down Expand Up @@ -135,6 +139,46 @@ def get_braze_external_id(self, email):

return None

def get_braze_external_id_batch(self, emails, alias_label):
"""
Check via /users/export/ids if the provided emails have external ids defined in Braze,
associated with the account via an alias.
https://www.braze.com/docs/api/endpoints/export/user_data/post_users_identifier/
"Up to 50 external_ids or user_aliases can be included in a single request.
Should you want to specify device_id or email_address
only one of either identifier can be included per request."
Arguments:
emails (list(str)): e.g. ['test1@example.com', 'test1@example.com']
alias_label (str): e.g. "my-business-segment-label"
Returns:
external_id (dict(str -> str): external_ids (string of lms_user_id) by email,
for any existing external ids.
"""
external_ids_by_email = {}
for email_batch in self._chunks(emails, GET_EXTERNAL_IDS_CHUNK_SIZE):
user_aliases = [
{
'alias_label': alias_label,
'alias_name': email,
}
for email in email_batch
]
payload = {
'user_aliases': user_aliases,
'fields_to_export': ['external_id', 'email']
}
logger.info('batch identify braze users request payload: %s', payload)

response = self._make_request(payload, BrazeAPIEndpoints.EXPORT_IDS, REQUEST_TYPE_POST)

for identified_user in response['users']:
external_ids_by_email[identified_user['email']] = identified_user['external_id']

logger.info(f'external ids from batch identify braze users response: {external_ids_by_email}')
return external_ids_by_email

def identify_users(self, aliases_to_identify):
"""
Identify unidentified (alias-only) users.
Expand Down Expand Up @@ -221,12 +265,13 @@ def create_braze_alias(self, emails, alias_label, attributes=None):
user_aliases = []
attributes = attributes or []

external_ids_by_email = self.get_braze_external_id_batch(emails, alias_label)
for email in emails:
user_alias = {
'alias_label': alias_label,
'alias_name': email,
}
braze_external_id = self.get_braze_external_id(email)
braze_external_id = external_ids_by_email.get(email)
# Adding a user alias for an existing user requires an external_id to be
# included in the new user alias object.
# http://web.archive.org/web/20231005191135/https://www.braze.com/docs/api/endpoints/user_data/post_user_alias#response
Expand Down
4 changes: 4 additions & 0 deletions braze/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class BrazeAPIEndpoints:
REQUEST_TYPE_POST = 'post'
TRACK_USER_COMPONENT_CHUNK_SIZE = 75
USER_ALIAS_CHUNK_SIZE = 50

# https://www.braze.com/docs/api/endpoints/export/user_data/post_users_identifier/?tab=all%20fields
GET_EXTERNAL_IDS_CHUNK_SIZE = 50

UNSUBSCRIBED_STATE = 'unsubscribed'
UNSUBSCRIBED_EMAILS_API_LIMIT = 500
UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION = 'desc'
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ multi_line_output = 3

[wheel]
universal = 1

[flake8]
max_line_length = 120
25 changes: 16 additions & 9 deletions tests/braze/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
import responses

from braze.client import BrazeClient
from braze.constants import UNSUBSCRIBED_EMAILS_API_LIMIT, UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION, BrazeAPIEndpoints
from braze.constants import (
GET_EXTERNAL_IDS_CHUNK_SIZE,
UNSUBSCRIBED_EMAILS_API_LIMIT,
UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION,
BrazeAPIEndpoints,
)
from braze.exceptions import (
BrazeBadRequestError,
BrazeClientError,
Expand Down Expand Up @@ -247,11 +252,12 @@ def test_create_braze_alias_user_exists(self):
Tests that calls to to /users/alias/new and /users/track are not made
if a Braze user already exists for the given email.
"""
test_email = 'test@example.com'
existing_enternal_id = '1'
responses.add(
responses.POST,
self.EXPORT_ID_URL,
json={'users': [{'external_id': existing_enternal_id}], 'message': 'success'},
json={'users': [{'external_id': existing_enternal_id, 'email': test_email}], 'message': 'success'},
status=201
)
responses.add(
Expand All @@ -268,7 +274,7 @@ def test_create_braze_alias_user_exists(self):
)

self.client.create_braze_alias(
emails=['test@example.com'],
emails=[test_email],
alias_label='alias_label',
attributes=[]
)
Expand Down Expand Up @@ -310,16 +316,17 @@ def test_create_braze_alias_batching(self):
alias_label='alias_label'
)

create_alias_batch_size = math.ceil(len(emails) / 50)
track_user_batch_size = math.ceil(len(emails) / 75)
create_alias_num_batches = math.ceil(len(emails) / 50)
track_user_num_batches = math.ceil(len(emails) / 75)
identify_users_num_batches = math.ceil(len(emails) / GET_EXTERNAL_IDS_CHUNK_SIZE)

assert len(responses.calls) == len(emails) + create_alias_batch_size + track_user_batch_size
assert len(responses.calls) == identify_users_num_batches + create_alias_num_batches + track_user_num_batches
export_id_calls = [call for call in responses.calls if call.request.url == self.EXPORT_ID_URL]
new_alias_calls = [call for call in responses.calls if call.request.url == self.NEW_ALIAS_URL]
track_user_calls = [call for call in responses.calls if call.request.url == self.USERS_TRACK_URL]
assert len(export_id_calls) == len(emails)
assert len(new_alias_calls) == create_alias_batch_size
assert len(track_user_calls) == track_user_batch_size
assert len(export_id_calls) == identify_users_num_batches
assert len(new_alias_calls) == create_alias_num_batches
assert len(track_user_calls) == track_user_num_batches

@ddt.data(
{'emails': [], 'subject': 'subject', 'body': 'body', 'from_email': 'support@email.com'},
Expand Down

0 comments on commit 9e7a0d9

Please sign in to comment.