Skip to content

Commit 9e7a0d9

Browse files
committed
feat: check for external ids in batchs when creating aliases.
ENT-8325 | Query `/users/export/ids` using batches of alias records when checking for existing accounts/external ids in `create_braze_alias()`.
1 parent e46cd8f commit 9e7a0d9

File tree

7 files changed

+76
-18
lines changed

7 files changed

+76
-18
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Change Log
1414
Unreleased
1515
~~~~~~~~~~
1616

17+
[0.2.0]
18+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
19+
feat: check for external ids in batchs when creating aliases.
20+
1721
[0.1.8]
1822
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1923
fix: always create an alias for existing profiles

Makefile

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,9 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy
4949

5050
quality: ## check coding style with pycodestyle and pylint
5151
touch tests/__init__.py
52-
pylint braze tests test_utils manage.py *.py
52+
pylint braze tests test_utils *.py
5353
pycodestyle braze tests *.py
54-
pydocstyle braze tests *.py
55-
isort --check-only --diff --recursive tests test_utils braze *.py test_settings.py
56-
python setup.py bdist_wheel
57-
twine check dist/*
58-
make selfcheck
59-
54+
isort --check-only --diff --recursive tests test_utils braze *.py
6055

6156
requirements: ## install development environment requirements
6257
pip install -r requirements/pip.txt

braze/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Python client for interacting with Braze APIs.
33
"""
44

5-
__version__ = '0.1.8'
5+
__version__ = '0.2.0'

braze/client.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
"""
44
import datetime
55
import json
6+
import logging
67
from collections import deque
78
from urllib.parse import urljoin
89

910
import requests
1011

1112
from braze.constants import (
13+
GET_EXTERNAL_IDS_CHUNK_SIZE,
1214
REQUEST_TYPE_GET,
1315
REQUEST_TYPE_POST,
1416
TRACK_USER_COMPONENT_CHUNK_SIZE,
@@ -29,6 +31,8 @@
2931
BrazeUnauthorizedError,
3032
)
3133

34+
logger = logging.getLogger(__name__)
35+
3236

3337
class BrazeClient:
3438
"""
@@ -135,6 +139,46 @@ def get_braze_external_id(self, email):
135139

136140
return None
137141

142+
def get_braze_external_id_batch(self, emails, alias_label):
143+
"""
144+
Check via /users/export/ids if the provided emails have external ids defined in Braze,
145+
associated with the account via an alias.
146+
147+
https://www.braze.com/docs/api/endpoints/export/user_data/post_users_identifier/
148+
"Up to 50 external_ids or user_aliases can be included in a single request.
149+
Should you want to specify device_id or email_address
150+
only one of either identifier can be included per request."
151+
152+
Arguments:
153+
emails (list(str)): e.g. ['test1@example.com', 'test1@example.com']
154+
alias_label (str): e.g. "my-business-segment-label"
155+
Returns:
156+
external_id (dict(str -> str): external_ids (string of lms_user_id) by email,
157+
for any existing external ids.
158+
"""
159+
external_ids_by_email = {}
160+
for email_batch in self._chunks(emails, GET_EXTERNAL_IDS_CHUNK_SIZE):
161+
user_aliases = [
162+
{
163+
'alias_label': alias_label,
164+
'alias_name': email,
165+
}
166+
for email in email_batch
167+
]
168+
payload = {
169+
'user_aliases': user_aliases,
170+
'fields_to_export': ['external_id', 'email']
171+
}
172+
logger.info('batch identify braze users request payload: %s', payload)
173+
174+
response = self._make_request(payload, BrazeAPIEndpoints.EXPORT_IDS, REQUEST_TYPE_POST)
175+
176+
for identified_user in response['users']:
177+
external_ids_by_email[identified_user['email']] = identified_user['external_id']
178+
179+
logger.info(f'external ids from batch identify braze users response: {external_ids_by_email}')
180+
return external_ids_by_email
181+
138182
def identify_users(self, aliases_to_identify):
139183
"""
140184
Identify unidentified (alias-only) users.
@@ -221,12 +265,13 @@ def create_braze_alias(self, emails, alias_label, attributes=None):
221265
user_aliases = []
222266
attributes = attributes or []
223267

268+
external_ids_by_email = self.get_braze_external_id_batch(emails, alias_label)
224269
for email in emails:
225270
user_alias = {
226271
'alias_label': alias_label,
227272
'alias_name': email,
228273
}
229-
braze_external_id = self.get_braze_external_id(email)
274+
braze_external_id = external_ids_by_email.get(email)
230275
# Adding a user alias for an existing user requires an external_id to be
231276
# included in the new user alias object.
232277
# http://web.archive.org/web/20231005191135/https://www.braze.com/docs/api/endpoints/user_data/post_user_alias#response

braze/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ class BrazeAPIEndpoints:
2424
REQUEST_TYPE_POST = 'post'
2525
TRACK_USER_COMPONENT_CHUNK_SIZE = 75
2626
USER_ALIAS_CHUNK_SIZE = 50
27+
28+
# https://www.braze.com/docs/api/endpoints/export/user_data/post_users_identifier/?tab=all%20fields
29+
GET_EXTERNAL_IDS_CHUNK_SIZE = 50
30+
2731
UNSUBSCRIBED_STATE = 'unsubscribed'
2832
UNSUBSCRIBED_EMAILS_API_LIMIT = 500
2933
UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION = 'desc'

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ multi_line_output = 3
66

77
[wheel]
88
universal = 1
9+
10+
[flake8]
11+
max_line_length = 120

tests/braze/test_client.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
import responses
1010

1111
from braze.client import BrazeClient
12-
from braze.constants import UNSUBSCRIBED_EMAILS_API_LIMIT, UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION, BrazeAPIEndpoints
12+
from braze.constants import (
13+
GET_EXTERNAL_IDS_CHUNK_SIZE,
14+
UNSUBSCRIBED_EMAILS_API_LIMIT,
15+
UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION,
16+
BrazeAPIEndpoints,
17+
)
1318
from braze.exceptions import (
1419
BrazeBadRequestError,
1520
BrazeClientError,
@@ -247,11 +252,12 @@ def test_create_braze_alias_user_exists(self):
247252
Tests that calls to to /users/alias/new and /users/track are not made
248253
if a Braze user already exists for the given email.
249254
"""
255+
test_email = 'test@example.com'
250256
existing_enternal_id = '1'
251257
responses.add(
252258
responses.POST,
253259
self.EXPORT_ID_URL,
254-
json={'users': [{'external_id': existing_enternal_id}], 'message': 'success'},
260+
json={'users': [{'external_id': existing_enternal_id, 'email': test_email}], 'message': 'success'},
255261
status=201
256262
)
257263
responses.add(
@@ -268,7 +274,7 @@ def test_create_braze_alias_user_exists(self):
268274
)
269275

270276
self.client.create_braze_alias(
271-
emails=['test@example.com'],
277+
emails=[test_email],
272278
alias_label='alias_label',
273279
attributes=[]
274280
)
@@ -310,16 +316,17 @@ def test_create_braze_alias_batching(self):
310316
alias_label='alias_label'
311317
)
312318

313-
create_alias_batch_size = math.ceil(len(emails) / 50)
314-
track_user_batch_size = math.ceil(len(emails) / 75)
319+
create_alias_num_batches = math.ceil(len(emails) / 50)
320+
track_user_num_batches = math.ceil(len(emails) / 75)
321+
identify_users_num_batches = math.ceil(len(emails) / GET_EXTERNAL_IDS_CHUNK_SIZE)
315322

316-
assert len(responses.calls) == len(emails) + create_alias_batch_size + track_user_batch_size
323+
assert len(responses.calls) == identify_users_num_batches + create_alias_num_batches + track_user_num_batches
317324
export_id_calls = [call for call in responses.calls if call.request.url == self.EXPORT_ID_URL]
318325
new_alias_calls = [call for call in responses.calls if call.request.url == self.NEW_ALIAS_URL]
319326
track_user_calls = [call for call in responses.calls if call.request.url == self.USERS_TRACK_URL]
320-
assert len(export_id_calls) == len(emails)
321-
assert len(new_alias_calls) == create_alias_batch_size
322-
assert len(track_user_calls) == track_user_batch_size
327+
assert len(export_id_calls) == identify_users_num_batches
328+
assert len(new_alias_calls) == create_alias_num_batches
329+
assert len(track_user_calls) == track_user_num_batches
323330

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

0 commit comments

Comments
 (0)