From 81e4b1761090e2c8f3c1a8e75ce5b6a4fa42b05b Mon Sep 17 00:00:00 2001 From: Vitor Oliveira Date: Wed, 15 Apr 2026 14:40:29 +0100 Subject: [PATCH 1/6] Braze cloud mode -New merge users action --- .../src/destinations/braze/index.ts | 26 +- .../__snapshots__/snapshot.test.ts.snap | 45 +++ .../braze/mergeUsers/__tests__/index.test.ts | 355 ++++++++++++++++++ .../mergeUsers/__tests__/snapshot.test.ts | 110 ++++++ .../braze/mergeUsers/generated-types.ts | 60 +++ .../destinations/braze/mergeUsers/index.ts | 128 +++++++ .../src/destinations/braze/utils.ts | 62 +++ 7 files changed, 775 insertions(+), 11 deletions(-) create mode 100644 packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/braze/mergeUsers/index.ts diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index 85b600ee994..1b45b8e771f 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -18,6 +18,8 @@ import triggerCanvas from './triggerCanvas' import { EVENT_NAMES } from './ecommerce/constants' import upsertCatalogItem from './upsertCatalogItem' +import mergeUsers from './mergeUsers' + const destination: DestinationDefinition = { name: 'Braze Cloud Mode (Actions)', slug: 'actions-braze-cloud', @@ -96,12 +98,14 @@ const destination: DestinationDefinition = { triggerCampaign, triggerCanvas, ecommerce, - ecommerceSingleProduct + ecommerceSingleProduct, + mergeUsers }, presets: [ { name: 'Track Calls', - subscribe: 'type = "track" and event != "Order Completed" and event != "Checkout Started" and event != "Order Refunded" and event != "Order Cancelled" and event != "Product Viewed"', + subscribe: + 'type = "track" and event != "Order Completed" and event != "Checkout Started" and event != "Order Refunded" and event != "Order Cancelled" and event != "Product Viewed"', partnerAction: 'trackEvent', mapping: defaultValues(trackEvent.fields), type: 'automatic' @@ -110,9 +114,9 @@ const destination: DestinationDefinition = { name: 'Order Placed (beta)', subscribe: 'event = "Order Completed"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_PLACED, + name: EVENT_NAMES.ORDER_PLACED, metadata: { order_status_url: { '@path': '$.properties.order_status_url' } } @@ -123,9 +127,9 @@ const destination: DestinationDefinition = { name: 'Checkout Started (beta)', subscribe: 'event = "Checkout Started"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.CHECKOUT_STARTED, + name: EVENT_NAMES.CHECKOUT_STARTED, metadata: { checkout_url: { '@path': '$.properties.checkout_url' } } @@ -136,9 +140,9 @@ const destination: DestinationDefinition = { name: 'Order Refunded (beta)', subscribe: 'event = "Order Refunded"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_REFUNDED, + name: EVENT_NAMES.ORDER_REFUNDED, metadata: { order_status_url: { '@path': '$.properties.order_status_url' } } @@ -149,9 +153,9 @@ const destination: DestinationDefinition = { name: 'Order Cancelled (beta)', subscribe: 'event = "Order Cancelled"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_CANCELLED, + name: EVENT_NAMES.ORDER_CANCELLED, metadata: { order_status_url: { '@path': '$.properties.order_status_url' } } @@ -162,7 +166,7 @@ const destination: DestinationDefinition = { name: 'Product Viewed (beta)', subscribe: 'event = "Product Viewed"', partnerAction: 'ecommerceSingleProduct', - mapping: { + mapping: { ...defaultValues(ecommerceSingleProduct.fields), name: EVENT_NAMES.PRODUCT_VIEWED }, diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 00000000000..91c9b17acfd --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for Braze's mergeUsers destination action: all fields 1`] = ` +Object { + "merge_updates": Array [ + Object { + "identifier_to_keep": Object { + "braze_id": "8$QYsxcxpjIn)Y", + "email": "gonwijpel@vep.il", + "external_id": "8$QYsxcxpjIn)Y", + "phone": "8$QYsxcxpjIn)Y", + "user_alias": Object { + "alias_label": "segment", + "alias_name": "keep-alias", + }, + }, + "identifier_to_merge": Object { + "braze_id": "8$QYsxcxpjIn)Y", + "email": "gonwijpel@vep.il", + "external_id": "8$QYsxcxpjIn)Y", + "phone": "8$QYsxcxpjIn)Y", + "user_alias": Object { + "alias_label": "segment", + "alias_name": "merge-alias", + }, + }, + }, + ], +} +`; + +exports[`Testing snapshot for Braze's mergeUsers destination action: required fields 1`] = ` +Object { + "merge_updates": Array [ + Object { + "identifier_to_keep": Object { + "external_id": "user-to-keep", + }, + "identifier_to_merge": Object { + "external_id": "user-to-merge", + }, + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/index.test.ts new file mode 100644 index 00000000000..fc39bae3003 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/index.test.ts @@ -0,0 +1,355 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) + +const settings = { + app_id: 'my-app-id', + api_key: 'my-api-key', + endpoint: 'https://rest.iad-01.braze.com' +} + +describe('Braze.mergeUsers', () => { + afterEach(() => { + nock.cleanAll() + }) + + it('should merge users with external_id identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track', + userId: 'user-to-keep-123' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].data).toMatchObject({ message: 'success' }) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + ] + }) + }) + + it('should merge users with user_alias identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + user_alias: { + alias_name: 'merge-alias', + alias_label: 'segment' + } + }, + identifier_to_keep: { + user_alias: { + alias_name: 'keep-alias', + alias_label: 'segment' + } + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + user_alias: { + alias_name: 'merge-alias', + alias_label: 'segment' + } + }, + identifier_to_keep: { + user_alias: { + alias_name: 'keep-alias', + alias_label: 'segment' + } + } + } + ] + }) + }) + + it('should merge users with email identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + email: 'merge@example.com' + }, + identifier_to_keep: { + email: 'keep@example.com' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + email: 'merge@example.com' + }, + identifier_to_keep: { + email: 'keep@example.com' + } + } + ] + }) + }) + + it('should merge users with braze_id identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + braze_id: 'braze-merge-id-123' + }, + identifier_to_keep: { + braze_id: 'braze-keep-id-456' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + braze_id: 'braze-merge-id-123' + }, + identifier_to_keep: { + braze_id: 'braze-keep-id-456' + } + } + ] + }) + }) + + it('should merge users with phone identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + phone: '+14155551234' + }, + identifier_to_keep: { + phone: '+14155555678' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + phone: '+14155551234' + }, + identifier_to_keep: { + phone: '+14155555678' + } + } + ] + }) + }) + + it('should merge users with mixed identifier types', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track', + userId: 'user-to-keep-123' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + email: 'merge@example.com' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + email: 'merge@example.com' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + ] + }) + }) + + it('should throw error when identifier_to_merge has no valid identifier', async () => { + const event = createTestEvent({ + type: 'track' + }) + + await expect( + testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: {}, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + }) + ).rejects.toThrowError( + 'Identifier to Merge must specify one of: external_id, user_alias, braze_id, email, or phone.' + ) + }) + + it('should throw error when identifier_to_keep has no valid identifier', async () => { + const event = createTestEvent({ + type: 'track' + }) + + await expect( + testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: {} + } + }) + ).rejects.toThrowError( + 'Identifier to Keep must specify one of: external_id, user_alias, braze_id, email, or phone.' + ) + }) + + it('should handle incomplete user_alias (missing required fields)', async () => { + const event = createTestEvent({ + type: 'track' + }) + + await expect( + testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + user_alias: { + alias_name: 'test' + // missing alias_label + } + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + }) + ).rejects.toThrowError( + 'Identifier to Merge must specify one of: external_id, user_alias, braze_id, email, or phone.' + ) + }) + + it('should use default mapping from userId for identifier_to_keep', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track', + userId: 'user-to-keep-123' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: { + external_id: { + '@path': '$.userId' + } + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + ] + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/snapshot.test.ts new file mode 100644 index 00000000000..263bd2efb7a --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/snapshot.test.ts @@ -0,0 +1,110 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'mergeUsers' +const destinationSlug = 'Braze' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [, settingsData] = generateTestData(seedName, destination, action, true) + + // Provide custom event data with valid identifiers + const customEventData = { + identifier_to_merge: { + external_id: 'user-to-merge' + }, + identifier_to_keep: { + external_id: 'user-to-keep' + } + } + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: customEventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: customEventData, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + // Customize the nested objects to include all identifier types + const customEventData = { + ...eventData, + identifier_to_merge: { + external_id: eventData.identifier_to_merge?.external_id || 'merge-external-id', + braze_id: eventData.identifier_to_merge?.braze_id || 'merge-braze-id', + email: eventData.identifier_to_merge?.email || 'merge@example.com', + phone: eventData.identifier_to_merge?.phone || '+14155551234', + user_alias: { + alias_name: 'merge-alias', + alias_label: 'segment' + } + }, + identifier_to_keep: { + external_id: eventData.identifier_to_keep?.external_id || 'keep-external-id', + braze_id: eventData.identifier_to_keep?.braze_id || 'keep-braze-id', + email: eventData.identifier_to_keep?.email || 'keep@example.com', + phone: eventData.identifier_to_keep?.phone || '+14155555678', + user_alias: { + alias_name: 'keep-alias', + alias_label: 'segment' + } + } + } + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: customEventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: customEventData, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts new file mode 100644 index 00000000000..2567bcc30a5 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts @@ -0,0 +1,60 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * User identifier for the user to be merged (the user to be deprecated). Must specify one of: External ID, User Alias, Braze ID, Email, or Phone. See [the docs](https://www.braze.com/docs/api/endpoints/user_data/post_users_merge/). + */ + identifier_to_merge: { + /** + * The external ID of the user to merge + */ + external_id?: string + /** + * The user alias object identifying the user to merge + */ + user_alias?: { + alias_name?: string + alias_label?: string + } + /** + * The Braze ID of the user to merge + */ + braze_id?: string + /** + * The email address of the user to merge + */ + email?: string + /** + * The phone number of the user to merge in E.164 format (e.g., +14155552671) + */ + phone?: string + } + /** + * User identifier for the user to keep (the target user). Must specify one of: External ID, User Alias, Braze ID, Email, or Phone. See [the docs](https://www.braze.com/docs/api/endpoints/user_data/post_users_merge/). + */ + identifier_to_keep: { + /** + * The external ID of the user to keep + */ + external_id?: string + /** + * The user alias object identifying the user to keep + */ + user_alias?: { + alias_name?: string + alias_label?: string + } + /** + * The Braze ID of the user to keep + */ + braze_id?: string + /** + * The email address of the user to keep + */ + email?: string + /** + * The phone number of the user to keep in E.164 format (e.g., +14155552671) + */ + phone?: string + } +} diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts new file mode 100644 index 00000000000..3d4aa6669b1 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts @@ -0,0 +1,128 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { mergeUsers } from '../utils' + +const action: ActionDefinition = { + title: 'Merge Users', + description: + 'Merge one identified user into another identified user. The merge will occur asynchronously and can take between 5-10 minutes.', + fields: { + identifier_to_merge: { + label: 'Identifier to Merge', + description: + 'User identifier for the user to be merged (the user to be deprecated). Must specify one of: External ID, User Alias, Braze ID, Email, or Phone. See [the docs](https://www.braze.com/docs/api/endpoints/user_data/post_users_merge/).', + type: 'object', + required: true, + defaultObjectUI: 'keyvalue', + additionalProperties: false, + properties: { + external_id: { + label: 'External ID', + description: 'The external ID of the user to merge', + type: 'string' + }, + user_alias: { + label: 'User Alias', + description: 'The user alias object identifying the user to merge', + type: 'object', + properties: { + alias_name: { + label: 'Alias Name', + type: 'string' + }, + alias_label: { + label: 'Alias Label', + type: 'string' + } + } + }, + braze_id: { + label: 'Braze ID', + description: 'The Braze ID of the user to merge', + type: 'string' + }, + email: { + label: 'Email', + description: 'The email address of the user to merge', + type: 'string', + format: 'email' + }, + phone: { + label: 'Phone', + description: 'The phone number of the user to merge in E.164 format (e.g., +14155552671)', + type: 'string' + } + } + }, + identifier_to_keep: { + label: 'Identifier to Keep', + description: + 'User identifier for the user to keep (the target user). Must specify one of: External ID, User Alias, Braze ID, Email, or Phone. See [the docs](https://www.braze.com/docs/api/endpoints/user_data/post_users_merge/).', + type: 'object', + required: true, + defaultObjectUI: 'keyvalue', + additionalProperties: false, + properties: { + external_id: { + label: 'External ID', + description: 'The external ID of the user to keep', + type: 'string', + default: { + '@path': '$.userId' + } + }, + user_alias: { + label: 'User Alias', + description: 'The user alias object identifying the user to keep', + type: 'object', + properties: { + alias_name: { + label: 'Alias Name', + type: 'string' + }, + alias_label: { + label: 'Alias Label', + type: 'string' + } + } + }, + braze_id: { + label: 'Braze ID', + description: 'The Braze ID of the user to keep', + type: 'string' + }, + email: { + label: 'Email', + description: 'The email address of the user to keep', + type: 'string', + format: 'email' + }, + phone: { + label: 'Phone', + description: 'The phone number of the user to keep in E.164 format (e.g., +14155552671)', + type: 'string' + } + }, + default: { + external_id: { + '@path': '$.userId' + }, + braze_id: { + '@path': '$.context.traits.brazeId' + }, + email: { + '@path': '$.context.traits.email' + }, + phone: { + '@path': '$.context.traits.phone' + } + } + } + }, + perform: (request, { settings, payload }) => { + return mergeUsers(request, settings, payload) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/braze/utils.ts b/packages/destination-actions/src/destinations/braze/utils.ts index e9a863cefb1..63a76db07c4 100644 --- a/packages/destination-actions/src/destinations/braze/utils.ts +++ b/packages/destination-actions/src/destinations/braze/utils.ts @@ -6,6 +6,7 @@ import action from './trackPurchase' import { Payload as TrackEventPayload } from './trackEvent/generated-types' import { Payload as TrackPurchasePayload } from './trackPurchase/generated-types' import { Payload as UpdateUserProfilePayload } from './updateUserProfile/generated-types' +import { Payload as MergeUsersPayload } from './mergeUsers/generated-types' import { getUserAlias } from './userAlias' import { HTTPError } from '@segment/actions-core' import { MAX_BATCH_SIZE } from './constants' @@ -659,6 +660,67 @@ async function handleBrazeAPIResponse( } } +export function mergeUsers(request: RequestClient, settings: Settings, payload: MergeUsersPayload) { + // Validate identifier_to_merge + const mergeUserAlias = getUserAlias(payload.identifier_to_merge?.user_alias) + const hasMergeIdentifier = + payload.identifier_to_merge?.external_id || + mergeUserAlias || + payload.identifier_to_merge?.braze_id || + payload.identifier_to_merge?.email || + payload.identifier_to_merge?.phone + + if (!hasMergeIdentifier) { + throw new IntegrationError( + 'Identifier to Merge must specify one of: external_id, user_alias, braze_id, email, or phone.', + 'Missing required identifier', + 400 + ) + } + + // Validate identifier_to_keep + const keepUserAlias = getUserAlias(payload.identifier_to_keep?.user_alias) + const hasKeepIdentifier = + payload.identifier_to_keep?.external_id || + keepUserAlias || + payload.identifier_to_keep?.braze_id || + payload.identifier_to_keep?.email || + payload.identifier_to_keep?.phone + + if (!hasKeepIdentifier) { + throw new IntegrationError( + 'Identifier to Keep must specify one of: external_id, user_alias, braze_id, email, or phone.', + 'Missing required identifier', + 400 + ) + } + + // Build the merge update object + const mergeUpdate: Record = { + identifier_to_merge: { + ...(payload.identifier_to_merge?.external_id && { external_id: payload.identifier_to_merge.external_id }), + ...(mergeUserAlias && { user_alias: mergeUserAlias }), + ...(payload.identifier_to_merge?.braze_id && { braze_id: payload.identifier_to_merge.braze_id }), + ...(payload.identifier_to_merge?.email && { email: payload.identifier_to_merge.email }), + ...(payload.identifier_to_merge?.phone && { phone: payload.identifier_to_merge.phone }) + }, + identifier_to_keep: { + ...(payload.identifier_to_keep?.external_id && { external_id: payload.identifier_to_keep.external_id }), + ...(keepUserAlias && { user_alias: keepUserAlias }), + ...(payload.identifier_to_keep?.braze_id && { braze_id: payload.identifier_to_keep.braze_id }), + ...(payload.identifier_to_keep?.email && { email: payload.identifier_to_keep.email }), + ...(payload.identifier_to_keep?.phone && { phone: payload.identifier_to_keep.phone }) + } + } + + return request(`${settings.endpoint}/users/merge`, { + method: 'post', + json: { + merge_updates: [mergeUpdate] + } + }) +} + export function generateMultiStatusError(batchSize: number, errorMessage: string): MultiStatusResponse { const multiStatusResponse = new MultiStatusResponse() From 9ea55a174309faaca132ce41d132aa108c89938a Mon Sep 17 00:00:00 2001 From: Vitor Oliveira Date: Wed, 15 Apr 2026 14:59:29 +0100 Subject: [PATCH 2/6] addressing issues raised by copilot --- .../src/destinations/braze/mergeUsers/index.ts | 18 +++++++++++++++--- .../src/destinations/braze/utils.ts | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts index 3d4aa6669b1..f4125102232 100644 --- a/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts @@ -109,13 +109,25 @@ const action: ActionDefinition = { '@path': '$.userId' }, braze_id: { - '@path': '$.context.traits.brazeId' + '@if': { + exists: { '@path': '$.context.traits.brazeId' } + }, + then: { '@path': '$.context.traits.brazeId' }, + else: { '@path': '$.properties.brazeId' } }, email: { - '@path': '$.context.traits.email' + '@if': { + exists: { '@path': '$.context.traits.email' } + }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } }, phone: { - '@path': '$.context.traits.phone' + '@if': { + exists: { '@path': '$.context.traits.phone' } + }, + then: { '@path': '$.context.traits.phone' }, + else: { '@path': '$.properties.phone' } } } } diff --git a/packages/destination-actions/src/destinations/braze/utils.ts b/packages/destination-actions/src/destinations/braze/utils.ts index 63a76db07c4..48b0b3da7e9 100644 --- a/packages/destination-actions/src/destinations/braze/utils.ts +++ b/packages/destination-actions/src/destinations/braze/utils.ts @@ -673,7 +673,7 @@ export function mergeUsers(request: RequestClient, settings: Settings, payload: if (!hasMergeIdentifier) { throw new IntegrationError( 'Identifier to Merge must specify one of: external_id, user_alias, braze_id, email, or phone.', - 'Missing required identifier', + 'Missing required fields', 400 ) } From 538097d8ba951b1cbcd6c859288e5978d0f6b6ab Mon Sep 17 00:00:00 2001 From: Vitor Oliveira Date: Wed, 15 Apr 2026 15:13:29 +0100 Subject: [PATCH 3/6] addressing issues raised by copilot --- packages/destination-actions/src/destinations/braze/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/braze/utils.ts b/packages/destination-actions/src/destinations/braze/utils.ts index 48b0b3da7e9..70ec3d6fb65 100644 --- a/packages/destination-actions/src/destinations/braze/utils.ts +++ b/packages/destination-actions/src/destinations/braze/utils.ts @@ -690,7 +690,7 @@ export function mergeUsers(request: RequestClient, settings: Settings, payload: if (!hasKeepIdentifier) { throw new IntegrationError( 'Identifier to Keep must specify one of: external_id, user_alias, braze_id, email, or phone.', - 'Missing required identifier', + 'Missing required fields', 400 ) } From dd570f65f888130175a21231c4571aff3372663e Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Tue, 21 Apr 2026 14:36:55 +0100 Subject: [PATCH 4/6] updating the new action as per review session with Rohan and Joe --- .../braze/mergeUsers/generated-types.ts | 70 ++--- .../destinations/braze/mergeUsers/index.ts | 250 ++++++++++-------- .../destinations/braze/mergeUsers/types.ts | 26 ++ .../src/destinations/braze/userAlias.ts | 2 +- .../src/destinations/braze/utils.ts | 87 +++--- 5 files changed, 234 insertions(+), 201 deletions(-) create mode 100644 packages/destination-actions/src/destinations/braze/mergeUsers/types.ts diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts index 2567bcc30a5..02845070f7d 100644 --- a/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts @@ -2,59 +2,45 @@ export interface Payload { /** - * User identifier for the user to be merged (the user to be deprecated). Must specify one of: External ID, User Alias, Braze ID, Email, or Phone. See [the docs](https://www.braze.com/docs/api/endpoints/user_data/post_users_merge/). + * The type of identifier for the user to be merged. One of: external_id, user_alias, braze_id, email, or phone. */ - identifier_to_merge: { - /** - * The external ID of the user to merge - */ - external_id?: string - /** - * The user alias object identifying the user to merge - */ - user_alias?: { - alias_name?: string - alias_label?: string - } - /** - * The Braze ID of the user to merge - */ - braze_id?: string + previousIdType: string + /** + * The value of the identifier for the user to be merged. + */ + previousIdValue?: string + /** + * The value of the user alias identifier for the user to be merged. Required if the previous identifier type is user_alias. + */ + previousAliasIdValue?: { /** - * The email address of the user to merge + * The label of the user alias for the user to be merged. */ - email?: string + alias_label: string /** - * The phone number of the user to merge in E.164 format (e.g., +14155552671) + * The name of the user alias for the user to be merged. */ - phone?: string + alias_name: string } /** - * User identifier for the user to keep (the target user). Must specify one of: External ID, User Alias, Braze ID, Email, or Phone. See [the docs](https://www.braze.com/docs/api/endpoints/user_data/post_users_merge/). + * The type of identifier for the user to be kept. One of: external_id, user_alias, braze_id, email, or phone. */ - identifier_to_keep: { - /** - * The external ID of the user to keep - */ - external_id?: string - /** - * The user alias object identifying the user to keep - */ - user_alias?: { - alias_name?: string - alias_label?: string - } - /** - * The Braze ID of the user to keep - */ - braze_id?: string + keepIdType: string + /** + * The value of the identifier for the user to be kept. + */ + keepIdValue?: string + /** + * The value of the user alias identifier for the user to be kept. Required if the keep identifier type is user_alias. + */ + keepAliasIdValue?: { /** - * The email address of the user to keep + * The label of the user alias for the user to be kept. */ - email?: string + alias_label: string /** - * The phone number of the user to keep in E.164 format (e.g., +14155552671) + * The name of the user alias for the user to be kept. */ - phone?: string + alias_name: string } } diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts index f4125102232..ee22fe9d3b4 100644 --- a/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts @@ -7,127 +7,165 @@ const action: ActionDefinition = { title: 'Merge Users', description: 'Merge one identified user into another identified user. The merge will occur asynchronously and can take between 5-10 minutes.', + defaultSubscription: 'type = "alias"', fields: { - identifier_to_merge: { - label: 'Identifier to Merge', + previousIdType: { + label: 'Type of Identifier to merge', description: - 'User identifier for the user to be merged (the user to be deprecated). Must specify one of: External ID, User Alias, Braze ID, Email, or Phone. See [the docs](https://www.braze.com/docs/api/endpoints/user_data/post_users_merge/).', - type: 'object', + 'The type of identifier for the user to be merged. One of: external_id, user_alias, braze_id, email, or phone.', + type: 'string', required: true, - defaultObjectUI: 'keyvalue', - additionalProperties: false, - properties: { - external_id: { - label: 'External ID', - description: 'The external ID of the user to merge', - type: 'string' - }, - user_alias: { - label: 'User Alias', - description: 'The user alias object identifying the user to merge', - type: 'object', - properties: { - alias_name: { - label: 'Alias Name', - type: 'string' - }, - alias_label: { - label: 'Alias Label', - type: 'string' - } + choices: [ + { label: 'External ID', value: 'external_id' }, + { label: 'User Alias', value: 'user_alias' }, + { label: 'Braze ID', value: 'braze_id' }, + { label: 'Email', value: 'email' }, + { label: 'Phone', value: 'phone' } + ], + default: 'external_id' + }, + previousIdValue: { + label: 'ID value to merge', + description: 'The value of the identifier for the user to be merged.', + type: 'string', + required: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is_not', + value: 'user_alias' } - }, - braze_id: { - label: 'Braze ID', - description: 'The Braze ID of the user to merge', - type: 'string' - }, - email: { - label: 'Email', - description: 'The email address of the user to merge', + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is_not', + value: 'user_alias' + } + ] + }, + default: '$.previousId' + }, + previousAliasIdValue: { + label: 'User Alias value to merge', + description: 'The value of the user alias identifier for the user to be merged. Required if the previous identifier type is user_alias.', + type: 'object', + required: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is', + value: 'user_alias' + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is', + value: 'user_alias' + } + ] + }, + properties: { + alias_label: { + label: 'User Alias Label', + description: 'The label of the user alias for the user to be merged.', type: 'string', - format: 'email' + required: true }, - phone: { - label: 'Phone', - description: 'The phone number of the user to merge in E.164 format (e.g., +14155552671)', - type: 'string' + alias_name: { + label: 'User Alias Name', + description: 'The name of the user alias for the user to be merged.', + type: 'string', + required: true } } }, - identifier_to_keep: { - label: 'Identifier to Keep', + keepIdType: { + label: 'Type of Identifier to keep', description: - 'User identifier for the user to keep (the target user). Must specify one of: External ID, User Alias, Braze ID, Email, or Phone. See [the docs](https://www.braze.com/docs/api/endpoints/user_data/post_users_merge/).', - type: 'object', + 'The type of identifier for the user to be kept. One of: external_id, user_alias, braze_id, email, or phone.', + type: 'string', required: true, - defaultObjectUI: 'keyvalue', - additionalProperties: false, - properties: { - external_id: { - label: 'External ID', - description: 'The external ID of the user to keep', - type: 'string', - default: { - '@path': '$.userId' + choices: [ + { label: 'External ID', value: 'external_id' }, + { label: 'User Alias', value: 'user_alias' }, + { label: 'Braze ID', value: 'braze_id' }, + { label: 'Email', value: 'email' }, + { label: 'Phone', value: 'phone' } + ], + default: 'external_id' + }, + keepIdValue: { + label: 'ID value to keep', + description: 'The value of the identifier for the user to be kept.', + type: 'string', + required: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is_not', + value: 'user_alias' } - }, - user_alias: { - label: 'User Alias', - description: 'The user alias object identifying the user to keep', - type: 'object', - properties: { - alias_name: { - label: 'Alias Name', - type: 'string' - }, - alias_label: { - label: 'Alias Label', - type: 'string' - } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is_not', + value: 'user_alias' } - }, - braze_id: { - label: 'Braze ID', - description: 'The Braze ID of the user to keep', - type: 'string' - }, - email: { - label: 'Email', - description: 'The email address of the user to keep', - type: 'string', - format: 'email' - }, - phone: { - label: 'Phone', - description: 'The phone number of the user to keep in E.164 format (e.g., +14155552671)', - type: 'string' - } + ] }, - default: { - external_id: { - '@path': '$.userId' - }, - braze_id: { - '@if': { - exists: { '@path': '$.context.traits.brazeId' } - }, - then: { '@path': '$.context.traits.brazeId' }, - else: { '@path': '$.properties.brazeId' } - }, - email: { - '@if': { - exists: { '@path': '$.context.traits.email' } - }, - then: { '@path': '$.context.traits.email' }, - else: { '@path': '$.properties.email' } + default: '$.userId' + }, + keepAliasIdValue: { + label: 'User Alias value to keep', + description: 'The value of the user alias identifier for the user to be kept. Required if the keep identifier type is user_alias.', + type: 'object', + required: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is', + value: 'user_alias' + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is', + value: 'user_alias' + } + ] + }, + properties: { + alias_label: { + label: 'User Alias Label', + description: 'The label of the user alias for the user to be kept.', + type: 'string', + required: true }, - phone: { - '@if': { - exists: { '@path': '$.context.traits.phone' } - }, - then: { '@path': '$.context.traits.phone' }, - else: { '@path': '$.properties.phone' } + alias_name: { + label: 'User Alias Name', + description: 'The name of the user alias for the user to be kept.', + type: 'string', + required: true } } } diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts new file mode 100644 index 00000000000..a9876294386 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts @@ -0,0 +1,26 @@ +export type MergeUsersJSON = { + identifier_to_merge: { + // Only one of the following + external_id?: string + user_alias?: { + alias_label: string + alias_name: string + } + braze_id?: string + email?: string + phone?: string + } + identifier_to_keep: { + // Only one of the following + external_id?: string + user_alias?: { + alias_label: string + alias_name: string + } + braze_id?: string + email?: string + phone?: string + } +} + +export type MergeIdentifierType = 'external_id' | 'user_alias' | 'braze_id' | 'email' | 'phone' \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/braze/userAlias.ts b/packages/destination-actions/src/destinations/braze/userAlias.ts index 285b6cecd72..081b122615b 100644 --- a/packages/destination-actions/src/destinations/braze/userAlias.ts +++ b/packages/destination-actions/src/destinations/braze/userAlias.ts @@ -1,4 +1,4 @@ -interface UserAlias { +export interface UserAlias { alias_name: string alias_label: string } diff --git a/packages/destination-actions/src/destinations/braze/utils.ts b/packages/destination-actions/src/destinations/braze/utils.ts index 70ec3d6fb65..b0b7e932554 100644 --- a/packages/destination-actions/src/destinations/braze/utils.ts +++ b/packages/destination-actions/src/destinations/braze/utils.ts @@ -1,4 +1,4 @@ -import { JSONLikeObject, ModifiedResponse, MultiStatusResponse, omit } from '@segment/actions-core' +import { JSONLikeObject, ModifiedResponse, MultiStatusResponse, omit, PayloadValidationError } from '@segment/actions-core' import { IntegrationError, RequestClient, removeUndefined } from '@segment/actions-core' import dayjs from 'dayjs' import { Settings } from './generated-types' @@ -7,7 +7,8 @@ import { Payload as TrackEventPayload } from './trackEvent/generated-types' import { Payload as TrackPurchasePayload } from './trackPurchase/generated-types' import { Payload as UpdateUserProfilePayload } from './updateUserProfile/generated-types' import { Payload as MergeUsersPayload } from './mergeUsers/generated-types' -import { getUserAlias } from './userAlias' +import { MergeUsersJSON, MergeIdentifierType } from './mergeUsers/types' +import { getUserAlias, UserAlias } from './userAlias' import { HTTPError } from '@segment/actions-core' import { MAX_BATCH_SIZE } from './constants' type DateInput = string | Date | number | null | undefined @@ -661,56 +662,21 @@ async function handleBrazeAPIResponse( } export function mergeUsers(request: RequestClient, settings: Settings, payload: MergeUsersPayload) { - // Validate identifier_to_merge - const mergeUserAlias = getUserAlias(payload.identifier_to_merge?.user_alias) - const hasMergeIdentifier = - payload.identifier_to_merge?.external_id || - mergeUserAlias || - payload.identifier_to_merge?.braze_id || - payload.identifier_to_merge?.email || - payload.identifier_to_merge?.phone - - if (!hasMergeIdentifier) { - throw new IntegrationError( - 'Identifier to Merge must specify one of: external_id, user_alias, braze_id, email, or phone.', - 'Missing required fields', - 400 - ) - } - - // Validate identifier_to_keep - const keepUserAlias = getUserAlias(payload.identifier_to_keep?.user_alias) - const hasKeepIdentifier = - payload.identifier_to_keep?.external_id || - keepUserAlias || - payload.identifier_to_keep?.braze_id || - payload.identifier_to_keep?.email || - payload.identifier_to_keep?.phone - - if (!hasKeepIdentifier) { - throw new IntegrationError( - 'Identifier to Keep must specify one of: external_id, user_alias, braze_id, email, or phone.', - 'Missing required fields', - 400 - ) - } - - // Build the merge update object - const mergeUpdate: Record = { - identifier_to_merge: { - ...(payload.identifier_to_merge?.external_id && { external_id: payload.identifier_to_merge.external_id }), - ...(mergeUserAlias && { user_alias: mergeUserAlias }), - ...(payload.identifier_to_merge?.braze_id && { braze_id: payload.identifier_to_merge.braze_id }), - ...(payload.identifier_to_merge?.email && { email: payload.identifier_to_merge.email }), - ...(payload.identifier_to_merge?.phone && { phone: payload.identifier_to_merge.phone }) - }, - identifier_to_keep: { - ...(payload.identifier_to_keep?.external_id && { external_id: payload.identifier_to_keep.external_id }), - ...(keepUserAlias && { user_alias: keepUserAlias }), - ...(payload.identifier_to_keep?.braze_id && { braze_id: payload.identifier_to_keep.braze_id }), - ...(payload.identifier_to_keep?.email && { email: payload.identifier_to_keep.email }), - ...(payload.identifier_to_keep?.phone && { phone: payload.identifier_to_keep.phone }) - } + const { + previousIdType, + previousIdValue, + previousAliasIdValue, + keepIdType, + keepIdValue, + keepAliasIdValue + } = payload + + const previousId = getMergeIdentifier(previousIdType as MergeIdentifierType, 'merge', previousIdValue, previousAliasIdValue) + const keepId = getMergeIdentifier(keepIdType as MergeIdentifierType, 'keep', keepIdValue, keepAliasIdValue) + + const mergeUpdate: MergeUsersJSON = { + identifier_to_merge: { [previousIdType]: previousId }, + identifier_to_keep: { [keepIdType]: keepId } } return request(`${settings.endpoint}/users/merge`, { @@ -721,6 +687,23 @@ export function mergeUsers(request: RequestClient, settings: Settings, payload: }) } +export function getMergeIdentifier(type: MergeIdentifierType, label: string, value?: string, aliasValue?: UserAlias): string | UserAlias { + if (type === 'user_alias') { + const { alias_label, alias_name } = aliasValue || {} + if (!alias_label || !alias_name) { + throw new PayloadValidationError(`When Type of Identifier to ${label} is user_alias, alias_label and alias_name must be provided.`) + } + return { alias_label, alias_name } + } + + if (!value) { + throw new PayloadValidationError(`ID value to ${label} value must be provided.`) + } + + return value +} + + export function generateMultiStatusError(batchSize: number, errorMessage: string): MultiStatusResponse { const multiStatusResponse = new MultiStatusResponse() From 599739a43ab4ff432cbda72add0a023b934a7133 Mon Sep 17 00:00:00 2001 From: Vitor Oliveira Date: Wed, 22 Apr 2026 15:18:13 +0200 Subject: [PATCH 5/6] changes to parameters --- .../destinations/braze/mergeUsers/index.ts | 84 ++++++++++++++++--- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts index ee22fe9d3b4..48de156c6f7 100644 --- a/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts @@ -3,6 +3,13 @@ import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { mergeUsers } from '../utils' +const prioritizationChoices = [ + { value: 'identified', label: 'Identified' }, + { value: 'unidentified', label: 'Unidentified' }, + { value: 'most_recently_updated', label: 'Most Recently Updated' }, + { value: 'least_recently_updated', label: 'Least Recently Updated' } +] + const action: ActionDefinition = { title: 'Merge Users', description: @@ -12,17 +19,18 @@ const action: ActionDefinition = { previousIdType: { label: 'Type of Identifier to merge', description: - 'The type of identifier for the user to be merged. One of: external_id, user_alias, braze_id, email, or phone.', + 'The type of identifier for the user to be merged. One of: external_id, user_alias, email, or phone.', type: 'string', required: true, choices: [ { label: 'External ID', value: 'external_id' }, { label: 'User Alias', value: 'user_alias' }, - { label: 'Braze ID', value: 'braze_id' }, { label: 'Email', value: 'email' }, { label: 'Phone', value: 'phone' } ], - default: 'external_id' + default: { + '@path': 'external_id' + } }, previousIdValue: { label: 'ID value to merge', @@ -48,11 +56,14 @@ const action: ActionDefinition = { } ] }, - default: '$.previousId' + default: { + '@path': '$.previousId' + } }, previousAliasIdValue: { label: 'User Alias value to merge', - description: 'The value of the user alias identifier for the user to be merged. Required if the previous identifier type is user_alias.', + description: + 'The value of the user alias identifier for the user to be merged. Required if the previous identifier type is user_alias.', type: 'object', required: { match: 'all', @@ -91,14 +102,12 @@ const action: ActionDefinition = { }, keepIdType: { label: 'Type of Identifier to keep', - description: - 'The type of identifier for the user to be kept. One of: external_id, user_alias, braze_id, email, or phone.', + description: 'The type of identifier for the user to be kept. One of: external_id, user_alias, email, or phone.', type: 'string', required: true, choices: [ { label: 'External ID', value: 'external_id' }, { label: 'User Alias', value: 'user_alias' }, - { label: 'Braze ID', value: 'braze_id' }, { label: 'Email', value: 'email' }, { label: 'Phone', value: 'phone' } ], @@ -128,11 +137,12 @@ const action: ActionDefinition = { } ] }, - default: '$.userId' + default: 'external_id' }, keepAliasIdValue: { label: 'User Alias value to keep', - description: 'The value of the user alias identifier for the user to be kept. Required if the keep identifier type is user_alias.', + description: + 'The value of the user alias identifier for the user to be kept. Required if the keep identifier type is user_alias.', type: 'object', required: { match: 'all', @@ -168,6 +178,60 @@ const action: ActionDefinition = { required: true } } + }, + keepIdPrioritization: { + label: 'Rule Prioritization', + description: 'Rule determining which user to merge if multiple users are found.', + type: 'string', + choices: prioritizationChoices, + required: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is', + value: ['email', 'phone'] + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is', + value: ['email', 'phone'] + } + ] + }, + default: 'identified' + }, + previousIdPrioritization: { + label: 'Rule Prioritization', + description: 'Rule determining which user to merge if multiple users are found.', + type: 'string', + choices: prioritizationChoices, + required: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is', + value: ['email', 'phone'] + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is', + value: ['email', 'phone'] + } + ] + }, + default: 'identified' } }, perform: (request, { settings, payload }) => { From a9e73d5103cf3731003129137028b4b577efc727 Mon Sep 17 00:00:00 2001 From: Vitor Oliveira Date: Wed, 22 Apr 2026 16:10:34 +0200 Subject: [PATCH 6/6] updating types --- .../src/destinations/braze/mergeUsers/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts index a9876294386..55db4a62b1d 100644 --- a/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts @@ -6,9 +6,9 @@ export type MergeUsersJSON = { alias_label: string alias_name: string } - braze_id?: string email?: string phone?: string + previousIdPrioritization?: string } identifier_to_keep: { // Only one of the following @@ -17,10 +17,10 @@ export type MergeUsersJSON = { alias_label: string alias_name: string } - braze_id?: string email?: string phone?: string + keepIdPrioritization?: string } } -export type MergeIdentifierType = 'external_id' | 'user_alias' | 'braze_id' | 'email' | 'phone' \ No newline at end of file +export type MergeIdentifierType = 'external_id' | 'user_alias' | 'email' | 'phone'