diff --git a/README.md b/README.md index 9539d20e3..f83592f55 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ This library currently supports a subset of the [Braze API endpoints](https://ww - [ ] /users/external_ids/rename - [ ] /users/external_ids/remove - [ ] /users/identify -- [ ] /users/track +- [x] /users/track ### Send messages diff --git a/src/Braze.test.ts b/src/Braze.test.ts index 0527b0927..091b2b230 100644 --- a/src/Braze.test.ts +++ b/src/Braze.test.ts @@ -2,6 +2,7 @@ import type { CampaignsTriggerSendObject, MessagesSendObject, TransactionalV1CampaignsSendObject, + UsersTrackObject, } from '.' import { Braze } from '.' import { request } from './common/request' @@ -79,3 +80,23 @@ it('calls transactional.v1.campaigns.send()', async () => { ) expect(mockedRequest).toBeCalledTimes(1) }) + +it('calls users.track()', async () => { + mockedRequest.mockResolvedValueOnce(response) + expect(await braze.users.track(body as UsersTrackObject)).toBe(response) + expect(mockedRequest).toBeCalledWith(`${apiUrl}/users/track`, body, options) + expect(mockedRequest).toBeCalledTimes(1) +}) + +it('calls users.track() with bulk', async () => { + mockedRequest.mockResolvedValueOnce(response) + expect(await braze.users.track(body as UsersTrackObject, true)).toBe(response) + expect(mockedRequest).toBeCalledWith(`${apiUrl}/users/track`, body, { + ...options, + headers: { + ...options.headers, + 'X-Braze-Bulk': 'true', + }, + }) + expect(mockedRequest).toBeCalledTimes(1) +}) diff --git a/src/Braze.ts b/src/Braze.ts index 58be6c3f8..f48e91455 100644 --- a/src/Braze.ts +++ b/src/Braze.ts @@ -1,6 +1,7 @@ import * as campaigns from './campaigns' import * as messages from './messages' import * as transactional from './transactional' +import * as users from './users' export class Braze { /** @@ -51,4 +52,9 @@ export class Braze { }, }, } + + users = { + track: (body: users.UsersTrackObject, bulk?: boolean) => + users.track(this.apiUrl, this.apiKey, body, bulk), + } } diff --git a/src/common/types.ts b/src/common/types.ts index af6c7297e..c704e6de8 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -51,6 +51,8 @@ type Comparison = export type TriggerProperties = object +export type Properties = object + export type Attributes = object export interface UserAlias { diff --git a/src/index.ts b/src/index.ts index 60f3d36e7..88a18cd20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export * from './campaigns/trigger/types' export * from './common/types' export * from './messages/types' export * from './transactional/v1/campaigns/types' +export * from './users/types' diff --git a/src/users/index.ts b/src/users/index.ts new file mode 100644 index 000000000..f3681a7d3 --- /dev/null +++ b/src/users/index.ts @@ -0,0 +1,2 @@ +export * from './track' +export * from './types' diff --git a/src/users/track.test.ts b/src/users/track.test.ts new file mode 100644 index 000000000..8da961dff --- /dev/null +++ b/src/users/track.test.ts @@ -0,0 +1,77 @@ +import { post } from '../common/request' +import { track } from '.' +import type { UsersTrackObject } from './types' + +jest.mock('../common/request') +const mockedPost = jest.mocked(post) + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('/users/track', () => { + const apiUrl = 'https://rest.iad-01.braze.com' + const apiKey = 'apiKey' + const body: UsersTrackObject = { + attributes: [ + { + external_id: 'user_identifier', + string_attribute: 'fruit', + boolean_attribute_1: true, + integer_attribute: 25, + array_attribute: ['banana', 'apple'], + }, + ], + events: [ + { + external_id: 'user_identifier', + app_id: 'app_identifier', + name: 'watched_trailer', + time: '2013-07-16T19:20:30+1:00', + }, + ], + purchases: [ + { + external_id: 'user_identifier', + app_id: 'app_identifier', + product_id: 'product_name', + currency: 'USD', + price: 12.12, + quantity: 6, + time: '2017-05-12T18:47:12Z', + properties: { + integer_property: 3, + string_property: 'Russell', + date_property: '2014-02-02T00:00:00Z', + }, + }, + ], + } + + const data = {} + + it('calls request with url and body', async () => { + mockedPost.mockResolvedValueOnce(data) + expect(await track(apiUrl, apiKey, body)).toBe(data) + expect(mockedPost).toBeCalledWith(`${apiUrl}/users/track`, body, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + }) + expect(mockedPost).toBeCalledTimes(1) + }) + + it('makes bulk update', async () => { + mockedPost.mockResolvedValueOnce(data) + expect(await track(apiUrl, apiKey, body, true)).toBe(data) + expect(mockedPost).toBeCalledWith(`${apiUrl}/users/track`, body, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + 'X-Braze-Bulk': 'true', + }, + }) + expect(mockedPost).toBeCalledTimes(1) + }) +}) diff --git a/src/users/track.ts b/src/users/track.ts new file mode 100644 index 000000000..87188d4c7 --- /dev/null +++ b/src/users/track.ts @@ -0,0 +1,36 @@ +import { post } from '../common/request' +import type { UsersTrackObject } from './types' + +/** + * User track. + * + * Use this endpoint to record custom events, purchases, and update user profile attributes. + * + * {@link https://www.braze.com/docs/api/endpoints/user_data/post_user_track/} + * + * @param apiUrl - Braze REST endpoint. + * @param apiKey - Braze API key. + * @param body - Request parameters. + * @param bulk - Bulk update. + * @returns - Braze response. + */ +export function track(apiUrl: string, apiKey: string, body: UsersTrackObject, bulk?: boolean) { + const options = { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + } + + if (bulk) { + ;(options.headers as Record)['X-Braze-Bulk'] = 'true' + } + + return post(`${apiUrl}/users/track`, body, options) as Promise<{ + message: string + attributes_processed?: number + events_processed?: number + purchases_processed?: number + errors?: object + }> +} diff --git a/src/users/types.ts b/src/users/types.ts new file mode 100644 index 000000000..2bde3dc1f --- /dev/null +++ b/src/users/types.ts @@ -0,0 +1,94 @@ +import type { Properties, UserAlias } from '../common/types' + +/** + * Request body for user track. + * + * {@link https://www.braze.com/docs/api/endpoints/user_data/post_user_track/#request-body} + */ +export interface UsersTrackObject { + attributes?: UserAttributesObject[] + events?: EventObject[] + purchases?: PurchaseObject[] +} + +/** + * User attributes object specification. + * + * {@link https://www.braze.com/docs/api/objects_filters/user_attributes_object} + */ +interface UserAttributesObject extends UserProfileField { + external_id?: string + user_alias?: UserAlias + braze_id?: string + _update_existing_only?: boolean + push_token_import?: boolean + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [custom_attribute: string]: any +} + +/** + * Braze user profile fields. + * + * {@link https://www.braze.com/docs/api/objects_filters/user_attributes_object} + */ +interface UserProfileField { + country?: string + current_location?: { + longitude: number + latitude: number + } + date_of_first_session?: string + date_of_last_session?: string + dob?: string + email?: string + email_subscribe?: 'opted_in' | 'unsubscribed' | 'subscribed' + email_open_tracking_disabled?: boolean + email_click_tracking_disabled?: boolean + external_id?: string + facebook?: string + first_name?: string + gender?: 'M' | 'F' | 'O' | 'N' | 'P' | null + home_city?: string + language?: string + last_name?: string + marked_email_as_spam_at?: string + phone?: string + push_subscribe?: 'opted_in' | 'unsubscribed' | 'subscribed' + time_zone?: string + twitter?: string +} + +/** + * Event object specification. + * + * {@link https://www.braze.com/docs/api/objects_filters/event_object/} + */ +interface EventObject { + external_id?: string + user_alias?: UserAlias + braze_id?: string + app_id?: string + name: string + time: string + properties?: Properties + _update_existing_only?: boolean +} + +/** + * Purchase object specification. + * + * {@link https://www.braze.com/docs/api/objects_filters/purchase_object/} + */ +interface PurchaseObject { + external_id?: string + user_alias?: UserAlias + braze_id?: string + app_id: string + product_id: string + currency: string + price: number + quantity?: number + time: string + properties?: Properties + _update_existing_only?: boolean +}