Skip to content

Commit bf0bc5c

Browse files
authored
chore: add metrics to count lookup failures per event for braze (rudderlabs#4875)
1 parent 56203fb commit bf0bc5c

4 files changed

Lines changed: 135 additions & 57 deletions

File tree

src/util/prometheus.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -635,13 +635,6 @@ class Prometheus {
635635
type: 'counter',
636636
labelNames: ['identifier_type', 'destination_id'],
637637
},
638-
{
639-
name: 'braze_lookup_failure_count',
640-
help: 'braze look-up failure count',
641-
type: 'counter',
642-
labelNames: ['http_status', 'destination_id'],
643-
},
644-
645638
{
646639
name: 'braze_lookup_time',
647640
help: 'braze look-up time',
@@ -665,6 +658,20 @@ class Prometheus {
665658
500, 600, 700, 800, 900, 1000,
666659
],
667660
},
661+
{
662+
name: 'braze_lookup_failure_identifiers',
663+
help: 'Count of identifiers that failed to lookup due to API failure',
664+
type: 'histogram',
665+
labelNames: ['destination_id', 'http_status'],
666+
buckets: [0, 10, 20, 30, 40, 50],
667+
},
668+
{
669+
name: 'braze_lookup_success_identifiers',
670+
help: 'Count of identifiers that successfully looked up due to API failure',
671+
type: 'histogram',
672+
labelNames: ['destination_id'],
673+
buckets: [0, 10, 20, 30, 40, 50],
674+
},
668675
{
669676
name: 'fb_custom_audience_event_having_all_null_field_values_for_a_user',
670677
help: 'fbcustomaudience event having all null field values for a user',

src/v0/destinations/braze/braze.util.test.js

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ describe('dedup utility tests', () => {
205205
// Mock the response from handleHttpRequest
206206
handleHttpRequest.mockResolvedValueOnce({
207207
processedResponse: {
208+
status: 200,
208209
response: {
209210
users: [
210211
{
@@ -272,31 +273,34 @@ describe('dedup utility tests', () => {
272273
// Call the function
273274
const users = await BrazeDedupUtility.doApiLookup(identfierChunks, { destination });
274275

275-
// Check the result
276+
// Check the result - now returns object with users and failedIdentifiers
276277
expect(users).toEqual([
277-
[
278-
{
279-
external_id: 'user1',
280-
email: 'user1@example.com',
281-
custom_attributes: {
282-
key1: 'value1',
278+
{
279+
users: [
280+
{
281+
external_id: 'user1',
282+
email: 'user1@example.com',
283+
custom_attributes: {
284+
key1: 'value1',
285+
},
283286
},
284-
},
285-
{
286-
external_id: 'user2',
287-
email: 'user2@example.com',
288-
custom_attributes: {
289-
key2: 'value2',
287+
{
288+
external_id: 'user2',
289+
email: 'user2@example.com',
290+
custom_attributes: {
291+
key2: 'value2',
292+
},
290293
},
291-
},
292-
{
293-
user_aliases: [{ alias_name: 'user3', alias_label: 'rudder_id' }],
294-
email: 'user3@example.com',
295-
custom_attributes: {
296-
key2: 'value3',
294+
{
295+
user_aliases: [{ alias_name: 'user3', alias_label: 'rudder_id' }],
296+
email: 'user3@example.com',
297+
custom_attributes: {
298+
key2: 'value3',
299+
},
297300
},
298-
},
299-
],
301+
],
302+
failedIdentifiers: [],
303+
},
300304
]);
301305

302306
// Check that handleHttpRequest was called once with the correct arguments
@@ -365,6 +369,7 @@ describe('dedup utility tests', () => {
365369
// Mock the handleHttpRequest function to return the same data every time it's called
366370
handleHttpRequest.mockImplementationOnce(() => ({
367371
processedResponse: {
372+
status: 200,
368373
response: {
369374
users: Array.from({ length: 50 }, (_, i) =>
370375
removeUndefinedAndNullAndEmptyValues({
@@ -385,6 +390,7 @@ describe('dedup utility tests', () => {
385390

386391
handleHttpRequest.mockImplementationOnce(() => ({
387392
processedResponse: {
393+
status: 200,
388394
response: {
389395
users: Array.from({ length: 50 }, (_, i) =>
390396
removeUndefinedAndNullAndEmptyValues({
@@ -405,6 +411,7 @@ describe('dedup utility tests', () => {
405411

406412
handleHttpRequest.mockImplementationOnce(() => ({
407413
processedResponse: {
414+
status: 200,
408415
response: {
409416
users: Array.from({ length: 10 }, (_, i) =>
410417
removeUndefinedAndNullAndEmptyValues({
@@ -426,8 +433,10 @@ describe('dedup utility tests', () => {
426433
const chunkedUserData = await BrazeDedupUtility.doApiLookup(identifierChunks, {
427434
destination,
428435
});
429-
const result = _.flatMap(chunkedUserData);
430-
expect(result).toHaveLength(110);
436+
// Each chunk now returns { users: [...], failedIdentifiers: [] }
437+
// So we need to extract users from each chunk and flatten
438+
const allUsers = chunkedUserData.flatMap((chunk) => chunk.users);
439+
expect(allUsers).toHaveLength(110);
431440
expect(handleHttpRequest).toHaveBeenCalledTimes(3);
432441
});
433442

@@ -486,14 +495,20 @@ describe('dedup utility tests', () => {
486495

487496
expect(handleHttpRequest).toHaveBeenCalledTimes(2);
488497
// Assert that the first chunk was successful and the second failed
489-
// The failed chunked will be returned as undefined
498+
// The failed chunk will return empty users array with failedIdentifiers
490499
expect(users).toEqual([
491-
[
492-
{ external_id: 'user1', email: 'user1@example.com' },
493-
{ alias_name: 'alias1', alias_label: 'rudder_id', email: 'alias1@example.com' },
494-
{ external_id: 'user2', email: 'user2@example.com' },
495-
],
496-
undefined,
500+
{
501+
users: [
502+
{ external_id: 'user1', email: 'user1@example.com' },
503+
{ alias_name: 'alias1', alias_label: 'rudder_id', email: 'alias1@example.com' },
504+
{ external_id: 'user2', email: 'user2@example.com' },
505+
],
506+
failedIdentifiers: [],
507+
},
508+
{
509+
users: [],
510+
failedIdentifiers: ['user3', 'alias2'],
511+
},
497512
]);
498513
});
499514
});
@@ -515,11 +530,21 @@ describe('dedup utility tests', () => {
515530
[{ alias_name: 'alias1', alias_label: 'rudder_id' }],
516531
[{ alias_name: 'alias2', alias_label: 'rudder_id' }],
517532
]);
533+
// doApiLookup now returns { users: [...], failedIdentifiers: [...] } for each chunk
518534
const doApiLookupMock = jest.spyOn(BrazeDedupUtility, 'doApiLookup').mockResolvedValue([
519-
[{ external_id: '123', custom_attributes: { key1: 'value1' } }],
520-
[{ external_id: '456', custom_attributes: { key2: 'value2' } }],
521-
undefined, // simulate failed api call
522-
[{ alias_name: 'alias2', custom_attributes: { key3: 'value3' } }],
535+
{
536+
users: [{ external_id: '123', custom_attributes: { key1: 'value1' } }],
537+
failedIdentifiers: [],
538+
},
539+
{
540+
users: [{ external_id: '456', custom_attributes: { key2: 'value2' } }],
541+
failedIdentifiers: [],
542+
},
543+
{ users: [], failedIdentifiers: ['alias1'] }, // simulate failed api call
544+
{
545+
users: [{ alias_name: 'alias2', custom_attributes: { key3: 'value3' } }],
546+
failedIdentifiers: [],
547+
},
523548
]);
524549

525550
// create input data for doLookup
@@ -532,12 +557,13 @@ describe('dedup utility tests', () => {
532557

533558
// call doLookup and verify the output
534559
const result = await BrazeDedupUtility.doLookup(inputs);
535-
expect(result).toEqual([
560+
// doLookup now returns { users: [...], failedIdentifiers: Set }
561+
expect(result.users).toEqual([
536562
{ external_id: '123', custom_attributes: { key1: 'value1' } },
537563
{ external_id: '456', custom_attributes: { key2: 'value2' } },
538-
undefined, // response of failed api call
539564
{ alias_name: 'alias2', custom_attributes: { key3: 'value3' } },
540565
]);
566+
expect(result.failedIdentifiers).toEqual(new Set(['alias1']));
541567

542568
// verify that the mocked functions were called with correct arguments
543569
expect(prepareInputForDedupMock).toHaveBeenCalledWith(inputs);

src/v0/destinations/braze/transform.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ function processTrackWithUserAttributes(
251251
processParams.userStore,
252252
payload,
253253
destination.ID,
254+
processParams.failedLookupIdentifiers,
254255
);
255256
if (dedupedAttributePayload) {
256257
requestJson.attributes = [dedupedAttributePayload];
@@ -300,6 +301,7 @@ function processTrackEvent(messageType, message, destination, mappingJson, proce
300301
processParams.userStore,
301302
attributePayload,
302303
destination.ID,
304+
processParams.failedLookupIdentifiers,
303305
);
304306
if (dedupedAttributePayload) {
305307
requestJson.attributes = [dedupedAttributePayload];
@@ -534,16 +536,20 @@ async function process(event, processParams = { userStore: new Map() }, reqMetad
534536

535537
const processRouterDest = async (inputs, reqMetadata) => {
536538
const userStore = new Map();
539+
let failedLookupIdentifiers = new Set();
537540
const { destination } = inputs[0];
538541
if (destination.Config.supportDedup) {
539-
let lookedUpUsers;
542+
let lookupResult;
540543
try {
541-
lookedUpUsers = await BrazeDedupUtility.doLookup(inputs);
544+
lookupResult = await BrazeDedupUtility.doLookup(inputs);
542545
} catch (error) {
543546
logger.error('Error while fetching user store', error);
544547
}
545548

546-
BrazeDedupUtility.updateUserStore(userStore, lookedUpUsers, destination.ID);
549+
if (lookupResult) {
550+
BrazeDedupUtility.updateUserStore(userStore, lookupResult.users, destination.ID);
551+
failedLookupIdentifiers = lookupResult.failedIdentifiers || new Set();
552+
}
547553
}
548554
// group events by userId or anonymousId and then call process
549555
const groupedInputs = lodash.groupBy(
@@ -563,6 +569,7 @@ const processRouterDest = async (inputs, reqMetadata) => {
563569
const respList = await simpleProcessRouterDestFunc(groupedInputs[id], process, reqMetadata, {
564570
userStore,
565571
identifyCallsArray,
572+
failedLookupIdentifiers,
566573
});
567574
return respList;
568575
});

src/v0/destinations/braze/util.js

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -257,13 +257,29 @@ const BrazeDedupUtility = {
257257
metadata,
258258
},
259259
);
260-
stats.counter('braze_lookup_failure_count', 1, {
261-
http_status: lookUpResponse.status,
262-
destination_id: destination.ID,
263-
});
264-
const { users } = lookUpResponse.response;
265260

266-
return users;
261+
// Track failed lookups and collect failed identifiers for non-2xx responses
262+
if (!isHttpStatusSuccess(lookUpResponse.status)) {
263+
// Collect failed identifiers (external_ids and alias_names)
264+
const failedIdentifiers = [
265+
...externalIdentifiers.map((id) => id.external_id),
266+
...aliasIdentifiers.map((id) => id.alias_name),
267+
];
268+
stats.histogram('braze_lookup_failure_identifiers', failedIdentifiers.length, {
269+
http_status: lookUpResponse.status,
270+
destination_id: destination.ID,
271+
});
272+
return { users: [], failedIdentifiers };
273+
}
274+
stats.histogram(
275+
'braze_lookup_success_identifiers',
276+
externalIdentifiers.length + aliasIdentifiers.length,
277+
{
278+
destination_id: destination.ID,
279+
},
280+
);
281+
const { users } = lookUpResponse.response;
282+
return { users: users || [], failedIdentifiers: [] };
267283
}),
268284
);
269285
},
@@ -273,24 +289,37 @@ const BrazeDedupUtility = {
273289
* uses the external_id field and the alias_name field to lookup users
274290
*
275291
* @param {*} inputs router transform input events array
276-
* @returns {Promise<Array>} array of braze user objects
292+
* @returns {Promise<{users: Array, failedIdentifiers: Set}>} object containing user objects and failed identifiers
277293
*/
278294
async doLookup(inputs) {
279295
const lookupStartTime = new Date();
280296
const { destination, metadata } = inputs[0];
281297
const { externalIdsToQuery, aliasIdsToQuery } = this.prepareInputForDedup(inputs);
282298
const identfierChunks = this.prepareChunksForDedup(externalIdsToQuery, aliasIdsToQuery);
283-
const chunkedUserData = await this.doApiLookup(identfierChunks, { destination, metadata });
299+
const chunkedResults = await this.doApiLookup(identfierChunks, { destination, metadata });
300+
301+
// Collect all users and failed identifiers from all chunks
302+
const allUsers = [];
303+
const failedIdentifiers = new Set();
304+
chunkedResults.forEach((result) => {
305+
if (result.users) {
306+
allUsers.push(...result.users);
307+
}
308+
if (result.failedIdentifiers) {
309+
result.failedIdentifiers.forEach((id) => failedIdentifiers.add(id));
310+
}
311+
});
312+
284313
stats.timing('braze_lookup_time', lookupStartTime, {
285314
destination_id: destination.ID,
286315
});
287-
stats.histogram('braze_lookup_count', chunkedUserData.length, {
316+
stats.histogram('braze_lookup_count', chunkedResults.length, {
288317
destination_id: destination.ID,
289318
});
290319
stats.histogram('braze_lookup_user_count', externalIdsToQuery.length + aliasIdsToQuery.length, {
291320
destination_id: destination.ID,
292321
});
293-
return _.flatMap(chunkedUserData);
322+
return { users: allUsers, failedIdentifiers };
294323
},
295324

296325
/**
@@ -424,9 +453,18 @@ const BrazeDedupUtility = {
424453
* @param {*} userStore
425454
* @param {*} payload
426455
* @param {*} destinationId
456+
* @param {Set} failedLookupIdentifiers - Set of identifiers that failed to lookup due to API failure
427457
* @returns
428458
*/
429-
const processDeduplication = (userStore, payload, destinationId) => {
459+
const processDeduplication = (userStore, payload, destinationId, failedLookupIdentifiers) => {
460+
// Check if this event's identifier failed to lookup due to API failure
461+
const identifier = payload.external_id || payload.user_alias?.alias_name;
462+
if (failedLookupIdentifiers && failedLookupIdentifiers.has(identifier)) {
463+
stats.increment('braze_dedup_skipped_due_to_lookup_failure_count', {
464+
destination_id: destinationId,
465+
});
466+
}
467+
430468
const dedupedAttributePayload = BrazeDedupUtility.deduplicate(payload, userStore);
431469
if (
432470
isDefinedAndNotNullAndNotEmpty(dedupedAttributePayload) &&

0 commit comments

Comments
 (0)