diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/__tests__/formatter.test.ts b/packages/destination-actions/src/destinations/tiktok-conversions/__tests__/formatter.test.ts index 2f96103d6b3..997a2aecd36 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/__tests__/formatter.test.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/__tests__/formatter.test.ts @@ -1,4 +1,4 @@ -import { formatEmails, formatPhones } from '../formatter' +import { formatEmails, formatPhones } from '../reportWebEvent/formatter' describe('Tiktok Conversions Formatter', () => { describe('formatEmails', () => { diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/__tests__/index.test.ts b/packages/destination-actions/src/destinations/tiktok-conversions/__tests__/index.test.ts index 1838dab9341..9c810254ecb 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/__tests__/index.test.ts @@ -436,7 +436,6 @@ describe('Tiktok Conversions', () => { properties: { email: 'coreytest1231@gmail.com', phone: '+1555-555-5555', - lead_id: '2229012621312', currency: 'USD', value: 100, query: 'shoes', @@ -527,7 +526,6 @@ describe('Tiktok Conversions', () => { email: ['eb9869a32b532840dd6aa714f7a872d21d6f650fc5aa933d9feefc64708969c7'], external_id: ['481f202262e9c5ccc48d24e60798fadaa5f6ff1f8369f7ab927c04c3aa682a7f'], ip: '0.0.0.0', - lead_id: '2229012621312', phone: ['910a625c4ba147b544e6bd2f267e130ae14c591b6ba9c25cb8573322dedbebd0'], ttclid: '123ATXSfe', user_agent: @@ -573,45 +571,7 @@ describe('Tiktok Conversions', () => { userId: 'testId123' }) - nock('https://business-api.tiktok.com/open_api/v1.3/event/track').post('/').reply(200, {}) - const responses = await testDestination.testAction('reportWebEvent', { - event, - settings, - useDefaultMappings: true, - mapping: { - event: 'AddToCart', - test_event_code: 'TEST04030', - contents: { - '@arrayPath': [ - '$.properties', - { - price: { - '@path': '$.price' - }, - quantity: { - '@path': '$.quantity' - }, - content_category: { - '@path': '$.category' - }, - content_id: { - '@path': '$.product_id' - }, - content_name: { - '@path': '$.name' - }, - brand: { - '@path': '$.brand' - } - } - ] - } - } - }) - - expect(responses.length).toBe(1) - expect(responses[0].status).toBe(200) - expect(responses[0].options.json).toMatchObject({ + const json = { data: [ { event: 'AddToCart', @@ -653,7 +613,46 @@ describe('Tiktok Conversions', () => { event_source_id: 'test', partner_name: 'Segment', test_event_code: 'TEST04030' + } + + nock('https://business-api.tiktok.com/open_api/v1.3/event/track').post('/', json).reply(200, {}) + const responses = await testDestination.testAction('reportWebEvent', { + event, + settings, + useDefaultMappings: true, + mapping: { + event: 'AddToCart', + test_event_code: 'TEST04030', + contents: { + '@arrayPath': [ + '$.properties', + { + price: { + '@path': '$.price' + }, + quantity: { + '@path': '$.quantity' + }, + content_category: { + '@path': '$.category' + }, + content_id: { + '@path': '$.product_id' + }, + content_name: { + '@path': '$.name' + }, + brand: { + '@path': '$.brand' + } + } + ] + } + } }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) }) }) }) diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/index.ts b/packages/destination-actions/src/destinations/tiktok-conversions/index.ts index 32c5f845144..69c12040db7 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/index.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/index.ts @@ -1,7 +1,6 @@ import type { DestinationDefinition } from '@segment/actions-core' import { defaultValues } from '@segment/actions-core' import type { Settings } from './generated-types' - import reportWebEvent from './reportWebEvent' const productProperties = { diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/constants.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/constants.ts new file mode 100644 index 00000000000..d62d43ed77c --- /dev/null +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/constants.ts @@ -0,0 +1,4 @@ +export const VEHICLE_FIELDS = 'vehicle_fields' +export const TRAVEL_FIELDS = 'travel_fields' +export const WEB = 'web' +export const CRM = 'crm' \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/common_fields.ts similarity index 78% rename from packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts rename to packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/common_fields.ts index c147678a12b..db8988fdebe 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/common_fields.ts @@ -1,6 +1,33 @@ import { InputField } from '@segment/actions-core' +import { VEHICLE_FIELDS, TRAVEL_FIELDS, WEB, CRM } from '../constants' -export const commonFields: Record = { +export const common_fields: Record = { + event_source: { + label: 'Event Source', + type: 'string', + description: + "The type of events you are uploading through TikTok Events API. Please see TikTok's [Events API documentation](https://ads.tiktok.com/marketing_api/docs?id=1701890979375106) for information on how to find this value. If no selection is made 'Web' is assumed.", + default: WEB, + choices: [ + { + value: WEB, + label: 'The events took place on your website and are measured by a Pixel Code.' + }, + { + value: CRM, + label: 'The lead events took place on a CRM system and are tracked by a CRM Event Set ID.' + } + ] + }, + event_spec_type: { + label: 'Additional Fields', + type: 'string', + description: 'Include fields for travel or vehicle events.', + choices: [ + { value: 'Travel Fields', label: TRAVEL_FIELDS }, + { value: 'Vehicle Fields', label: VEHICLE_FIELDS } + ] + }, event: { label: 'Event Name', type: 'string', @@ -36,8 +63,7 @@ export const commonFields: Record = { then: { '@path': '$.properties.phone' }, else: { '@path': '$.context.traits.phone' } } - }, - category: 'hashedPII' + } }, email: { label: 'Email', @@ -51,8 +77,7 @@ export const commonFields: Record = { then: { '@path': '$.properties.email' }, else: { '@path': '$.context.traits.email' } } - }, - category: 'hashedPII' + } }, first_name: { label: 'First Name', @@ -65,8 +90,7 @@ export const commonFields: Record = { then: { '@path': '$.properties.first_name' }, else: { '@path': '$.context.traits.first_name' } } - }, - category: 'hashedPII' + } }, last_name: { label: 'Last Name', @@ -79,8 +103,7 @@ export const commonFields: Record = { then: { '@path': '$.properties.last_name' }, else: { '@path': '$.context.traits.last_name' } } - }, - category: 'hashedPII' + } }, address: { label: 'Address', @@ -101,8 +124,7 @@ export const commonFields: Record = { zip_code: { label: 'Zip Code', type: 'string', - description: "The customer's Zip Code.", - category: 'hashedPII' + description: "The customer's Zip Code." }, state: { label: 'State', @@ -169,8 +191,7 @@ export const commonFields: Record = { then: { '@path': '$.userId' }, else: { '@path': '$.anonymousId' } } - }, - category: 'hashedPII' + } }, ttclid: { label: 'TikTok Click ID', @@ -198,13 +219,6 @@ export const commonFields: Record = { } } }, - lead_id: { - label: 'TikTok Lead ID', - description: - 'ID of TikTok leads. Every lead will have its own lead_id when exported from TikTok. This feature is in Beta. Please contact your TikTok representative to inquire regarding availability', - type: 'string', - default: { '@path': '$.properties.lead_id' } - }, locale: { label: 'Locale', description: @@ -251,7 +265,6 @@ export const commonFields: Record = { type: 'object', multiple: true, description: 'Related item details for the event.', - defaultObjectUI: 'keyvalue', properties: { price: { label: 'Price', @@ -285,6 +298,23 @@ export const commonFields: Record = { } } }, + content_ids: { + label: 'Content IDs', + description: "Product IDs associated with the event, such as SKUs. Do not populate this field if the 'Contents' field is populated. This field accepts a single string value or an array of string values.", + type: 'string', + multiple: true, + default: { + '@path': '$.properties.content_ids' + } + }, + num_items: { + label: 'Number of Items', + type: 'number', + description: 'Number of items when checkout was initiated. Used with the InitiateCheckout event.', + default: { + '@path': '$.properties.num_items' + } + }, content_type: { label: 'Content Type', description: @@ -343,5 +373,44 @@ export const commonFields: Record = { type: 'string', description: 'Use this field to specify that events should be test events rather than actual traffic. You can find your Test Event Code in your TikTok Events Manager under the "Test Event" tab. You\'ll want to remove your Test Event Code when sending real traffic through this integration.' + }, + delivery_category: { + label: 'Delivery Category', + type: 'string', + description: 'Category of the delivery.', + default: { + '@path': '$.properties.delivery_category' + }, + choices: [ + { value: 'in_store', label: 'In Store - Purchase requires customer to enter the store.' }, + { value: 'curbside', label: 'Curbside - Purchase requires curbside pickup.' }, + { value: 'home_delivery', label: 'Home Delivery - Purchase is delivered to the customer.' } + ] + }, + predicted_ltv: { + label: 'Prediected Lifetime Value', + type: 'number', + description: + 'Predicted lifetime value of a subscriber as defined by the advertiser and expressed as an exact value.', + default: { + '@path': '$.properties.predicted_ltv' + } + }, + search_string: { + label: 'Search String', + type: 'string', + description: 'The text string entered by the user for the search. Optionally used with the Search event.', + default: { + '@path': '$.properties.search_string' + }, + depends_on: { + conditions: [ + { + fieldKey: 'event', + operator: 'is', + value: 'Search' + } + ] + } } } diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/crm_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/crm_fields.ts new file mode 100644 index 00000000000..03316ab0640 --- /dev/null +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/crm_fields.ts @@ -0,0 +1,48 @@ +import { InputField } from '@segment/actions-core' +import { CRM } from '../constants' + +export const crm_fields: Record = { + lead_fields: { + label: 'CRM Fields', + type: 'object', + description: 'Fields related to CRM events.', + additionalProperties: false, + defaultObjectUI: 'keyvalue', + properties: { + lead_id: { + label: 'TikTok Lead ID', + description: + 'ID of TikTok leads. Every lead will have its own lead_id when exported from TikTok. This feature is in Beta. Please contact your TikTok representative to inquire regarding availability', + type: 'string' + }, + lead_event_source: { + label: 'TikTok Lead Event Source', + description: + 'Lead source of TikTok leads. Please set this field to the name of your CRM system, such as HubSpot or Salesforce.', + type: 'string' + } + }, + default: { + lead_id: { '@path': '$.properties.lead_id' }, + lead_event_source: { '@path': '$.properties.lead_event_source' } + }, + required: { + conditions: [ + { + fieldKey: 'event_source', + operator: 'is', + value: CRM + } + ] + }, + depends_on: { + conditions: [ + { + fieldKey: 'event_source', + operator: 'is', + value: CRM + } + ] + } + } +} diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/travel_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/travel_fields.ts new file mode 100644 index 00000000000..8278697d962 --- /dev/null +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/travel_fields.ts @@ -0,0 +1,222 @@ +import { InputField } from '@segment/actions-core' +import { TRAVEL_FIELDS, WEB } from '../constants' + +export const travel_fields: InputField = { + label: 'Travel Fields', + type: 'object', + description: 'Fields related to travel events.', + additionalProperties: false, + defaultObjectUI: 'keyvalue', + properties: { + city: { + label: 'Hotel City Location', + type: 'string', + description: 'Hotel city location.' + }, + region: { + label: 'Hotel Region', + type: 'string', + description: 'Hotel region location.' + }, + country: { + label: 'Hotel Country', + type: 'string', + description: 'Hotel country location.' + }, + checkin_date: { + label: 'Hotel Check-in Date', + type: 'string', + description: 'Hotel check-in date.' + }, + checkout_date: { + label: 'Hotel Check-out Date', + type: 'string', + description: 'Hotel check-out date.' + }, + num_adults: { + label: 'Number of Adults', + type: 'number', + description: 'Number of adults.' + }, + num_children: { + label: 'Number of Children', + type: 'number', + description: 'Number of children.' + }, + num_infants: { + label: 'Number of Infants', + type: 'number', + description: 'Number of infants flying.' + }, + suggested_hotels: { + label: 'Suggested Hotels', + description: 'Suggested hotels. This can be a single string value or an array of string values.', + type: 'string', + multiple: true + }, + departing_departure_date: { + label: 'Departure Date', + type: 'string', + description: + 'Date of flight departure. Accepted date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD' + }, + returning_departure_date: { + label: 'Arrival Date', + type: 'string', + description: + 'Date of return flight. Accepted date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD' + }, + origin_airport: { + label: 'Origin Airport', + type: 'string', + description: 'Origin airport.' + }, + destination_airport: { + label: 'Destination Airport', + type: 'string', + description: 'Destination airport.' + }, + destination_ids: { + label: 'Destination IDs', + description: + 'If a client has a destination catalog, the client can associate one or more destinations in the catalog with a specific flight event. For instance, link a particular route to a nearby museum and a nearby beach, both of which are destinations in the catalog. This field accepts a single string value or an array of string values.', + type: 'string', + multiple: true + }, + departing_arrival_date: { + label: 'Departing Arrival Date', + type: 'string', + description: + 'The date and time for arrival at the destination of the outbound journey. Accepted date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD' + }, + returning_arrival_date: { + label: 'Returning Arrival Date', + type: 'string', + description: + 'The date and time when the return journey is completed. Accepted date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD' + }, + travel_class: { + label: 'Flight Ticket Class', + type: 'string', + description: 'Class of the flight ticket, must be: "eco", "prem", "bus", "first".', + choices: [ + { value: 'eco', label: 'Economy' }, + { value: 'prem', label: 'Premium' }, + { value: 'bus', label: 'Bus' }, + { value: 'first', label: 'First' } + ] + }, + user_score: { + label: 'User Score', + type: 'number', + description: 'Represents the relative value of this potential customer to advertiser.' + }, + preferred_num_stops: { + label: 'Preferred Number of Stops', + type: 'integer', + description: 'The preferred number of stops the user is looking for. 0 for direct flight.' + }, + travel_start: { + label: 'Start Date of the Trip', + type: 'string', + description: + "The start date of user's trip. Accept date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD." + }, + travel_end: { + label: 'End Date of the Trip', + type: 'string', + description: + "The end date of user's trip. Accept date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD." + }, + suggested_destinations: { + label: 'Suggested Destination IDs', + description: + 'A list of IDs representing destination suggestions for this user. This parameter is not applicable for the Search event. This field accepts a single string value or an array of string values.', + type: 'string', + multiple: true + } + }, + default: { + city: { + '@path': '$.properties.city' + }, + region: { + '@path': '$.properties.region' + }, + country: { + '@path': '$.properties.country' + }, + checkin_date: { + '@path': '$.properties.checkin_date' + }, + checkout_date: { + '@path': '$.properties.checkout_date' + }, + num_adults: { + '@path': '$.properties.num_adults' + }, + num_children: { + '@path': '$.properties.num_children' + }, + num_infants: { + '@path': '$.properties.num_infants' + }, + suggested_hotels: { + '@path': '$.properties.suggested_hotels' // Confirmed this can be a single string or an array of strings + }, + departing_departure_date: { + '@path': '$.properties.departing_departure_date' + }, + returning_departure_date: { + '@path': '$.properties.returning_departure_date' + }, + origin_airport: { + '@path': '$.properties.origin_airport' + }, + destination_airiport: { + '@path': '$.properties.destination_airiport' + }, + destination_ids: { + '@path': '$.properties.destination_ids' // Confirmed this can be a single string or an array of strings + }, + departing_arrival_date: { + '@path': '$.properties.departing_arrival_date' + }, + returning_arrival_date: { + '@path': '$.properties.returning_arrival_date' + }, + travel_class: { + '@path': '$.properties.travel_class' + }, + user_score: { + '@path': '$.properties.user_score' + }, + preferred_num_stops: { + '@path': '$.properties.preferred_num_stops' + }, + travel_start: { + '@path': '$.properties.travel_start' + }, + travel_end: { + '@path': '$.properties.travel_end' + }, + suggested_destinations: { + '@path': '$.properties.suggested_destinations' // Confirmed this can be a single string or an array of strings + } + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'event_source', + operator: 'is', + value: WEB + }, + { + fieldKey: 'event_spec_type', + operator: 'is', + value: TRAVEL_FIELDS + } + ] + } +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/vehicle_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/vehicle_fields.ts new file mode 100644 index 00000000000..a60fdb0d577 --- /dev/null +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/vehicle_fields.ts @@ -0,0 +1,267 @@ +import { InputField } from '@segment/actions-core' +import { VEHICLE_FIELDS, WEB } from '../constants' + +export const vehicle_fields: InputField = { + label: 'Vehicle Fields', + type: 'object', + description: 'Fields related to vehicle events.', + additionalProperties: false, + defaultObjectUI: 'keyvalue', + properties: { + postal_code: { + label: 'Postal Code', + type: 'string', + description: 'Postal code for the vehicle location.' + }, + make: { + label: 'Make of the Vehicle', + type: 'string', + description: 'Vehicle make/brand/manufacturer.' + }, + model: { + label: 'Model of the Vehicle', + type: 'string', + description: 'Vehicle model.' + }, + year: { + label: 'Year of the Vehicle', + type: 'number', + description: 'Year the vehicle was laucned in yyyy format.' + }, + state_of_vehicle: { + label: 'State of the Vehicle', + type: 'string', + description: 'Vehicle status.', + choices: [ + { value: 'New', label: 'New' }, + { value: 'Used', label: 'Used' }, + { value: 'CPO', label: 'CPO' } + ] + }, + mileage_value: { + label: 'Mileage Value', + type: 'number', + description: 'Vehicle mileage (in km or miles). Zero (0) for new vehicle.' + }, + mileage_unit: { + label: 'Mileage Unit', + type: 'string', + description: 'Mileage unites in miles (MI) or kilometers (KM).', + choices: [ + { value: 'MI', label: 'Miles' }, + { value: 'KM', label: 'Kilometers' } + ] + }, + exterior_color: { + label: 'Exterior Color of the Vehicle', + type: 'string', + description: 'Vehicle exterior color.' + }, + transmission: { + label: 'Transmission Type of the Vehicle', + type: 'string', + description: 'Vehicle transmission type.', + choices: [ + { value: 'Automatic', label: 'Automatic' }, + { value: 'Manual', label: 'Manual' }, + { value: 'Other', label: 'Other' } + ] + }, + body_style: { + label: 'Body Type of the Vehicle', + type: 'string', + description: 'Vehicle body type.', + choices: [ + { value: 'Convertible', label: 'Convertible' }, + { value: 'Coupe', label: 'Coupe' }, + { value: 'Hatchback', label: 'Hatchback' }, + { value: 'Minivan', label: 'Minivan' }, + { value: 'Truck', label: 'Truck' }, + { value: 'SUV', label: 'SUV' }, + { value: 'Sedan', label: 'Sedan' }, + { value: 'Van', label: 'Van' }, + { value: 'Wagon', label: 'Wagon' }, + { value: 'Crossover', label: 'Crossover' }, + { value: 'Other', label: 'Other' } + ] + }, + fuel_type: { + label: 'Fuel Type of the Vehicle', + type: 'string', + description: 'Vehicle fuel type.', + choices: [ + { value: 'Diesel', label: 'Diesel' }, + { value: 'Electric', label: 'Electric' }, + { value: 'Flex', label: 'Flex' }, + { value: 'Gasoline', label: 'Gasoline' }, + { value: 'Hybrid', label: 'Hybrid' }, + { value: 'Other', label: 'Other' } + ] + }, + drivetrain: { + label: 'Drivetrain of the Vehicle', + type: 'string', + description: 'Vehicle drivetrain.', + choices: [ + { value: 'AWD', label: 'AWD' }, + { value: 'FOUR_WD', label: 'Four WD' }, + { value: 'FWD', label: 'FWD' }, + { value: 'RWD', label: 'RWD' }, + { value: 'TWO_WD', label: 'Two WD' }, + { value: 'Other', label: 'Other' } + ] + }, + preferred_price_range_min: { + label: 'Minimum Preferred Price', + type: 'number', + description: 'Minimum preferred price of the vehicle.' + }, + preferred_price_range_max: { + label: 'Maximum Preferred Price', + type: 'number', + description: 'Maximum preferred price of the vehicle.' + }, + trim: { + label: 'Trim of the Vehicle', + type: 'string', + description: 'Vehicle trim.' + }, + vin: { + label: 'VIN of the Vehicle', + type: 'string', + description: 'Vehicle identification number. Maximum characters: 17.' + }, + interior_color: { + label: 'Interior Color of the Vehicle', + type: 'string', + description: 'Vehicle interior color.' + }, + condition_of_vehicle: { + label: 'Condition of the Vehicle', + type: 'string', + description: 'Vehicle drivetrain.', + choices: [ + { value: 'Excellent', label: 'Excellent' }, + { value: 'Good', label: 'Good' }, + { value: 'Fair', label: 'Fair' }, + { value: 'Poor', label: 'Poor' }, + { value: 'Other', label: 'Other' } + ] + }, + viewcontent_type: { + label: 'Soft Lead Landing Page', + type: 'string', + description: 'Optional for ViewContent. Use viewcontent_type to differentiate between soft lead landing pages.', + depends_on: { + match: 'any', + conditions:[ + { fieldKey: 'event', operator: 'is', value: 'ViewContent' } + ] + } + }, + search_type: { + label: 'Other Search Page', + type: 'string', + description: + 'Optional for Search. Use search_type to differentiate other user searches (such as dealer lookup) from inventory search.', + depends_on: { + match: 'any', + conditions:[ + { fieldKey: 'event', operator: 'is', value: 'Search' } + ] + } + }, + registration_type: { + label: 'Other Registration Page', + type: 'string', + description: + 'Optional for CompleteRegistration. Use registration_type to differentiate between different types of customer registration on websites.', + depends_on: { + match: 'any', + conditions:[ + { fieldKey: 'event', operator: 'is', value: 'CompleteRegistration' } + ] + } + } + }, + default: { + postal_code: { + '@path': '$.properties.postal_code' + }, + make: { + '@path': '$.properties.make' + }, + model: { + '@path': '$.properties.model' + }, + year: { + '@path': '$.properties.year' + }, + state_of_vehicle: { + '@path': '$.properties.travel_class' + }, + mileage_value: { + '@path': '$.properties.mileage_value' + }, + mileage_unit: { + '@path': '$.properties.mileage_unit' + }, + exterior_color: { + '@path': '$.properties.exterior_color' + }, + transmission: { + '@path': '$.properties.transmission' + }, + body_style: { + '@path': '$.properties.body_style' + }, + fuel_type: { + '@path': '$.properties.fuel_type' + }, + drivetrain: { + '@path': '$.properties.travel_class' + }, + preferred_price_range_min: { + '@path': '$.properties.preferred_price_range_min' + }, + preferred_price_range_max: { + '@path': '$.properties.preferred_price_range_max' + }, + trim: { + '@path': '$.properties.trim' + }, + vin: { + '@path': '$.properties.vin' + }, + interior_color: { + '@path': '$.properties.interior_color' + }, + condition_of_vehicle: { + '@path': '$.properties.travel_class' + }, + viewcontent_type: { + '@path': '$.properties.viewcontent_type' + }, + search_type: { + '@path': '$.properties.search_type' + }, + registration_type: { + '@path': '$.properties.registration_type' + } + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'event_source', + operator: 'is', + value: WEB + }, + { + fieldKey: 'event_spec_type', + operator: 'is', + value: VEHICLE_FIELDS + } + ] + } +} diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/formatter.ts similarity index 97% rename from packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts rename to packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/formatter.ts index 4390d7bcf4b..d31e826259d 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/formatter.ts @@ -1,4 +1,4 @@ -import { processHashing } from '../../lib/hashing-utils' +import { processHashing } from '../../../lib/hashing-utils' /** * Convert emails to lower case, and hash in SHA256. */ diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/generated-types.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/generated-types.ts index af3ab4e75a1..b1e12aed700 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/generated-types.ts @@ -1,6 +1,14 @@ // Generated file. DO NOT MODIFY IT BY HAND. export interface Payload { + /** + * The type of events you are uploading through TikTok Events API. Please see TikTok's [Events API documentation](https://ads.tiktok.com/marketing_api/docs?id=1701890979375106) for information on how to find this value. If no selection is made 'Web' is assumed. + */ + event_source?: string + /** + * Include fields for travel or vehicle events. + */ + event_spec_type?: string /** * Conversion event name. Please refer to the "Supported Web Events" section on in TikTok’s [Events API documentation](https://ads.tiktok.com/marketing_api/docs?id=1701890979375106) for accepted event names. */ @@ -70,10 +78,6 @@ export interface Payload { * TikTok Cookie ID. If you also use Pixel SDK and have enabled cookies, Pixel SDK automatically saves a unique identifier in the `_ttp` cookie. The value of `_ttp` is used to match website visitor events with TikTok ads. You can extract the value of `_ttp` and attach the value here. To learn more about the `ttp` parameter, refer to [Events API 2.0 - Send TikTok Cookie](https://ads.tiktok.com/marketing_api/docs?id=%201771100936446977) (`_ttp`). */ ttp?: string - /** - * ID of TikTok leads. Every lead will have its own lead_id when exported from TikTok. This feature is in Beta. Please contact your TikTok representative to inquire regarding availability - */ - lead_id?: string /** * The BCP 47 language identifier. For reference, refer to the [IETF BCP 47 standardized code](https://www.rfc-editor.org/rfc/bcp/bcp47.txt). */ @@ -123,6 +127,14 @@ export interface Payload { */ brand?: string }[] + /** + * Product IDs associated with the event, such as SKUs. Do not populate this field if the 'Contents' field is populated. This field accepts a single string value or an array of string values. + */ + content_ids?: string[] + /** + * Number of items when checkout was initiated. Used with the InitiateCheckout event. + */ + num_items?: number /** * Type of the product item. When the `content_id` in the `Contents` field is specified as a `sku_id`, set this field to `product`. When the `content_id` in the `Contents` field is specified as an `item_group_id`, set this field to `product_group`. */ @@ -151,4 +163,211 @@ export interface Payload { * Use this field to specify that events should be test events rather than actual traffic. You can find your Test Event Code in your TikTok Events Manager under the "Test Event" tab. You'll want to remove your Test Event Code when sending real traffic through this integration. */ test_event_code?: string + /** + * Category of the delivery. + */ + delivery_category?: string + /** + * Predicted lifetime value of a subscriber as defined by the advertiser and expressed as an exact value. + */ + predicted_ltv?: number + /** + * The text string entered by the user for the search. Optionally used with the Search event. + */ + search_string?: string + /** + * Fields related to CRM events. + */ + lead_fields?: { + /** + * ID of TikTok leads. Every lead will have its own lead_id when exported from TikTok. This feature is in Beta. Please contact your TikTok representative to inquire regarding availability + */ + lead_id?: string + /** + * Lead source of TikTok leads. Please set this field to the name of your CRM system, such as HubSpot or Salesforce. + */ + lead_event_source?: string + } + /** + * Fields related to vehicle events. + */ + vehicle_fields?: { + /** + * Postal code for the vehicle location. + */ + postal_code?: string + /** + * Vehicle make/brand/manufacturer. + */ + make?: string + /** + * Vehicle model. + */ + model?: string + /** + * Year the vehicle was laucned in yyyy format. + */ + year?: number + /** + * Vehicle status. + */ + state_of_vehicle?: string + /** + * Vehicle mileage (in km or miles). Zero (0) for new vehicle. + */ + mileage_value?: number + /** + * Mileage unites in miles (MI) or kilometers (KM). + */ + mileage_unit?: string + /** + * Vehicle exterior color. + */ + exterior_color?: string + /** + * Vehicle transmission type. + */ + transmission?: string + /** + * Vehicle body type. + */ + body_style?: string + /** + * Vehicle fuel type. + */ + fuel_type?: string + /** + * Vehicle drivetrain. + */ + drivetrain?: string + /** + * Minimum preferred price of the vehicle. + */ + preferred_price_range_min?: number + /** + * Maximum preferred price of the vehicle. + */ + preferred_price_range_max?: number + /** + * Vehicle trim. + */ + trim?: string + /** + * Vehicle identification number. Maximum characters: 17. + */ + vin?: string + /** + * Vehicle interior color. + */ + interior_color?: string + /** + * Vehicle drivetrain. + */ + condition_of_vehicle?: string + /** + * Optional for ViewContent. Use viewcontent_type to differentiate between soft lead landing pages. + */ + viewcontent_type?: string + /** + * Optional for Search. Use search_type to differentiate other user searches (such as dealer lookup) from inventory search. + */ + search_type?: string + /** + * Optional for CompleteRegistration. Use registration_type to differentiate between different types of customer registration on websites. + */ + registration_type?: string + } + /** + * Fields related to travel events. + */ + travel_fields?: { + /** + * Hotel city location. + */ + city?: string + /** + * Hotel region location. + */ + region?: string + /** + * Hotel country location. + */ + country?: string + /** + * Hotel check-in date. + */ + checkin_date?: string + /** + * Hotel check-out date. + */ + checkout_date?: string + /** + * Number of adults. + */ + num_adults?: number + /** + * Number of children. + */ + num_children?: number + /** + * Number of infants flying. + */ + num_infants?: number + /** + * Suggested hotels. This can be a single string value or an array of string values. + */ + suggested_hotels?: string[] + /** + * Date of flight departure. Accepted date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD + */ + departing_departure_date?: string + /** + * Date of return flight. Accepted date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD + */ + returning_departure_date?: string + /** + * Origin airport. + */ + origin_airport?: string + /** + * Destination airport. + */ + destination_airport?: string + /** + * If a client has a destination catalog, the client can associate one or more destinations in the catalog with a specific flight event. For instance, link a particular route to a nearby museum and a nearby beach, both of which are destinations in the catalog. This field accepts a single string value or an array of string values. + */ + destination_ids?: string[] + /** + * The date and time for arrival at the destination of the outbound journey. Accepted date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD + */ + departing_arrival_date?: string + /** + * The date and time when the return journey is completed. Accepted date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD + */ + returning_arrival_date?: string + /** + * Class of the flight ticket, must be: "eco", "prem", "bus", "first". + */ + travel_class?: string + /** + * Represents the relative value of this potential customer to advertiser. + */ + user_score?: number + /** + * The preferred number of stops the user is looking for. 0 for direct flight. + */ + preferred_num_stops?: number + /** + * The start date of user's trip. Accept date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD. + */ + travel_start?: string + /** + * The end date of user's trip. Accept date formats: YYYYMMDD, YYYY-MM-DD, YYYY-MM-DDThh:mmTZD, and YYYY-MM-DDThh:mm:ssTZD. + */ + travel_end?: string + /** + * A list of IDs representing destination suggestions for this user. This parameter is not applicable for the Search event. This field accepts a single string value or an array of string values. + */ + suggested_destinations?: string[] + } } diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts index 28ea5bcb71c..6801360de6d 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts @@ -1,18 +1,24 @@ import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { commonFields } from '../common_fields' -import { performWebEvent } from '../utils' +import { common_fields } from './fields/common_fields' +import { crm_fields } from './fields/crm_fields' +import { travel_fields } from './fields/travel_fields' +import { vehicle_fields } from './fields/vehicle_fields' +import { send } from './utils' const action: ActionDefinition = { title: 'Report Web Event', description: 'Report Web events directly to TikTok. Data shared can power TikTok solutions like dynamic product ads, custom targeting, campaign optimization and attribution.', fields: { - ...commonFields + ...common_fields, + ...crm_fields, + vehicle_fields, + travel_fields }, perform: (request, { payload, settings }) => { - return performWebEvent(request, settings, payload) + return send(request, settings, payload) } } diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/types.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/types.ts new file mode 100644 index 00000000000..aab571ca0f1 --- /dev/null +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/types.ts @@ -0,0 +1,115 @@ +export interface TTJSON { + event_source: string + event_source_id: string + partner_name: string + test_event_code?: string + data: TTDataItem[] +} +interface TTDataItem { + event: string + event_time: number + event_id?: string + user: TTUser + properties: TTBaseProps & TTTravelProps & TTAutoProps + page?: { + url?: string + referrer?: string + } + limited_data_use: boolean + lead?: { + lead_id: string + lead_event_source?: string + } +} + +export interface TTUser { + external_id: string[] + phone: string[] + email: string[] + ttp?: string + ip?: string + user_agent?: string + locale?: string + first_name?: string + last_name?: string + city?: string + state?: string + country?: string + zip_code?: string + ttclid?: string +} + +export interface TTBaseProps { + contents: TTContentItem[] + content_type?: string + currency?: string + value?: number + query?: string + description?: string + order_id?: string + shop_id?: string + content_ids?: string[] + delivery_category?: string + num_items?: number + predicted_ltv?: number + search_string?: string +} + +export interface TTTravelProps { + city?: string + region?: string + country?: string + checkin_date?: string + checkout_date?: string + num_adults?: number + num_children?: number + num_infants?: number + suggested_hotels?: string[] + departing_departure_date?: string + returning_departure_date?: string + origin_airport?: string + destination_airiport?: string + destination_ids?: string[] + departing_arrival_date?: string + returning_arrival_date?: string + travel_class?: string + user_score?: number + preferred_num_stops?: number + travel_start?: string + travel_end?: string + suggested_destinations?: string[] +} + +export interface TTAutoProps { + postal_code?: string + make?: string + model?: string + year?: number + state_of_vehicle?: string + mileage?: { + value?: number + unit?: string + } + exterior_color?: string + transmission?: string + body_style?: string + fuel_type?: string + drivetrain?: string + preferred_price_range?: number[] + trim?: string + vin?: string + interior_color?: string + condition_of_vehicle?: string + viewcontent_type?: string + search_type?: string + registration_type?: string +} + +interface TTContentItem { + price?: number + quantity?: number + content_category?: string + content_id?: string + content_name?: string + brand?: string +} diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/utils.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/utils.ts new file mode 100644 index 00000000000..aeb239ccd9a --- /dev/null +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/utils.ts @@ -0,0 +1,275 @@ +import { RequestClient } from '@segment/actions-core' +import { Settings } from '../generated-types' +import { Payload } from './generated-types' +import { formatEmails, formatPhones, formatUserIds, formatString, formatAddress } from './formatter' +import { WEB, CRM, TRAVEL_FIELDS, VEHICLE_FIELDS } from './constants' +import { + TTJSON, + TTAutoProps, + TTBaseProps, + TTTravelProps, + TTUser +} from './types' + +export function send(request: RequestClient, settings: Settings, payload: Payload) { + const { + event, + event_id, + event_spec_type, + test_event_code, + url, + referrer, + limited_data_use, + lead_fields: { + lead_id, + lead_event_source + } = {} } = payload + + const user = getUser(payload) + const properties = getProps(payload) + const event_source = payload.event_source ?? WEB + + const requestJson: TTJSON = { + event_source: event_source ? event_source : WEB, + event_source_id: settings.pixelCode, + partner_name: 'Segment', + test_event_code: test_event_code ? test_event_code : undefined, + data: [ + { + event, + event_time: payload.timestamp + ? Math.floor(new Date(payload.timestamp).getTime() / 1000) + : Math.floor(new Date().getTime() / 1000), + event_id: event_id ? `${event_id}` : undefined, + user, + properties: { + ...properties, + ...(event_spec_type === TRAVEL_FIELDS && event_source === WEB ? getTravelProps(payload) : {}), + ...(event_spec_type === VEHICLE_FIELDS && event_source === WEB ? getAutoProps(payload) : {}) + }, + ...((url || referrer) ? { page: { ...(url && { url }), ...(referrer && { referrer }) } } : {}), + ...(event_source === CRM && typeof lead_id === 'string' + ? { + lead: { + lead_id, + ...(lead_event_source && { lead_event_source }) + } + } + : {}), + limited_data_use: typeof limited_data_use === 'boolean' ? limited_data_use : false + } + ] + } + + return request('https://business-api.tiktok.com/open_api/v1.3/event/track/', { + method: 'post', + json: requestJson + }) +} + +function getUser(payload: Payload): TTUser { + const { + phone_number, + email, + external_id, + url, + first_name, + last_name, + address, + ttclid, + ttp, + ip, + user_agent, + locale + } = payload + + const phone_numbers = formatPhones(phone_number) + const emails = formatEmails(email) + const userIds = formatUserIds(external_id) + + let payloadUrl, urlTtclid + if (url) { + try { + payloadUrl = new URL(url) + } catch (error) { + // invalid url + } + } + + if (payloadUrl) urlTtclid = payloadUrl.searchParams.get('ttclid') + + const requestUser: TTUser = { + external_id: userIds, + phone: phone_numbers, + email: emails, + first_name: formatString(first_name), + last_name: formatString(last_name), + city: formatAddress(address?.city), + state: formatAddress(address?.state), + country: formatAddress(address?.country), + zip_code: formatString(address?.zip_code), + ...(ttclid || urlTtclid ? { ttclid: urlTtclid ?? ttclid } : {}), + ...(ttp ? { ttp } : {}), + ...(ip ? { ip } : {}), + ...(user_agent ? { user_agent } : {}), + ...(locale ? { locale } : {}) + } + + return requestUser +} + +function getProps(payload: Payload): TTBaseProps { + const { + content_type, + currency, + value, + query, + description, + order_id, + shop_id, + content_ids, + delivery_category, + num_items, + predicted_ltv, + search_string, + contents + } = payload + + const requestProperties: TTBaseProps = { + contents: contents + ? contents.map(({ price, quantity, content_category, content_id, content_name, brand }) => ({ + price: price ?? undefined, + quantity: quantity ?? undefined, + content_category: content_category ?? undefined, + content_id: content_id ?? undefined, + content_name: content_name ?? undefined, + brand: brand ?? undefined, + })) + : [], + ...(content_type !== undefined && { content_type }), + ...(currency !== undefined && { currency }), + ...(value !== undefined && { value }), + ...(query !== undefined && { query }), + ...(description !== undefined && { description }), + ...(order_id !== undefined && { order_id }), + ...(shop_id !== undefined && { shop_id }), + ...(content_ids !== undefined && { content_ids }), + ...(delivery_category !== undefined && { delivery_category }), + ...(num_items !== undefined && { num_items }), + ...(predicted_ltv !== undefined && { predicted_ltv }), + ...(search_string !== undefined && { search_string }) + } + + return requestProperties +} + +function getTravelProps(payload: Payload): TTTravelProps { + const { + city, + region, + country, + checkin_date, + checkout_date, + num_adults, + num_children, + num_infants, + suggested_hotels, + departing_departure_date, + returning_departure_date, + origin_airport, + destination_airport, + destination_ids, + departing_arrival_date, + returning_arrival_date, + travel_class, + user_score, + preferred_num_stops, + travel_start, + travel_end, + suggested_destinations, + } = payload?.travelFields ?? {} + + const requestProperties: TTTravelProps = { + ...(city !== undefined && { city }), + ...(region !== undefined && { region }), + ...(country !== undefined && { country }), + ...(checkin_date !== undefined && { checkin_date }), + ...(checkout_date !== undefined && { checkout_date }), + ...(num_adults !== undefined && { num_adults }), + ...(num_children !== undefined && { num_children }), + ...(num_infants !== undefined && { num_infants }), + ...(suggested_hotels !== undefined && { suggested_hotels }), + ...(departing_departure_date !== undefined && { departing_departure_date }), + ...(returning_departure_date !== undefined && { returning_departure_date }), + ...(origin_airport !== undefined && { origin_airport }), + ...(destination_airport !== undefined && { destination_airport }), + ...(destination_ids !== undefined && { destination_ids }), + ...(departing_arrival_date !== undefined && { departing_arrival_date }), + ...(returning_arrival_date !== undefined && { returning_arrival_date }), + ...(travel_class !== undefined && { travel_class }), + ...(user_score !== undefined && { user_score }), + ...(preferred_num_stops !== undefined && { preferred_num_stops }), + ...(travel_start !== undefined && { travel_start }), + ...(travel_end !== undefined && { travel_end }), + ...(suggested_destinations !== undefined && { suggested_destinations }), + } + + return requestProperties +} + +function getAutoProps(payload: Payload): TTAutoProps { + const { + postal_code, + make, + model, + year, + state_of_vehicle, + mileage_unit, + mileage_value, + exterior_color, + transmission, + body_style, + fuel_type, + drivetrain, + preferred_price_range_min, + preferred_price_range_max, + trim, + vin, + interior_color, + condition_of_vehicle, + viewcontent_type, + search_type, + registration_type + } = payload?.autoFields ?? {} + + const requestProperties: TTAutoProps = { + ...(postal_code !== undefined && { postal_code }), + ...(make !== undefined && { make }), + ...(model !== undefined && { model }), + ...(year !== undefined && { year }), + ...(state_of_vehicle !== undefined && { state_of_vehicle }), + ...(exterior_color !== undefined && { exterior_color }), + ...(transmission !== undefined && { transmission }), + ...(body_style !== undefined && { body_style }), + ...(fuel_type !== undefined && { fuel_type }), + ...(drivetrain !== undefined && { drivetrain }), + ...(trim !== undefined && { trim }), + ...(vin !== undefined && { vin }), + ...(interior_color !== undefined && { interior_color }), + ...(condition_of_vehicle !== undefined && { condition_of_vehicle }), + ...(viewcontent_type !== undefined && { viewcontent_type }), + ...(search_type !== undefined && { search_type }), + ...(registration_type !== undefined && { registration_type }), + ...(mileage_unit !== undefined && typeof mileage_value === 'number' && { + mileage: { + unit: mileage_unit, + value: mileage_value + } + }), + ...(typeof preferred_price_range_min === 'number' && typeof preferred_price_range_max === 'number' && { + preferred_price_range: [preferred_price_range_min, preferred_price_range_max] + }) + } + + return requestProperties +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/types.ts b/packages/destination-actions/src/destinations/tiktok-conversions/types.ts deleted file mode 100644 index a2919120748..00000000000 --- a/packages/destination-actions/src/destinations/tiktok-conversions/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -export interface TikTokConversionsRequest { - event_source: string - event_source_id: string - partner_name: string - test_event_code?: string - data: TikTokConversionsData[] -} - -export interface TikTokConversionsData { - event: string - event_time: number - event_id?: string - user: TikTokConversionsUser - properties: TikTokConversionsProperties - page?: TikTokConversionsPage - limited_data_use: boolean -} - -export interface TikTokConversionsPage { - url?: string - referrer?: string -} - -export interface TikTokConversionsUser { - external_id: string[] - phone: string[] - email: string[] - ttp?: string - lead_id?: string - ip?: string - user_agent?: string - locale?: string - first_name?: string - last_name?: string - city?: string - state?: string - country?: string - zip_code?: string - ttclid?: string -} - -export interface TikTokConversionsProperties { - contents: TikTokConversionsContent[] - content_type?: string - currency?: string - value?: number - query?: string - description?: string - order_id?: string - shop_id?: string -} - -export interface TikTokConversionsContent { - price?: number - quantity?: number - content_category?: string - content_id?: string - content_name?: string - brand?: string -} diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts b/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts deleted file mode 100644 index f1f6d28ce6b..00000000000 --- a/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { RequestClient } from '@segment/actions-core' -import { Settings } from './generated-types' -import { Payload } from './reportWebEvent/generated-types' -import { formatEmails, formatPhones, formatUserIds, formatString, formatAddress } from './formatter' -import { - TikTokConversionsPage, - TikTokConversionsProperties, - TikTokConversionsRequest, - TikTokConversionsUser -} from './types' - -export function performWebEvent(request: RequestClient, settings: Settings, payload: Payload) { - const requestUser = validateRequestUser(payload) - const requestProperties = validateRequestProperties(payload) - const requestPage = validateRequestPage(payload) - - const requestJson: TikTokConversionsRequest = { - event_source: 'web', - event_source_id: settings.pixelCode, - partner_name: 'Segment', - test_event_code: payload.test_event_code ? payload.test_event_code : undefined, - data: [ - { - event: payload.event, - event_time: payload.timestamp - ? Math.floor(new Date(payload.timestamp).getTime() / 1000) - : Math.floor(new Date().getTime() / 1000), - event_id: payload.event_id ? `${payload.event_id}` : undefined, - user: requestUser, - properties: requestProperties, - page: requestPage, - limited_data_use: payload.limited_data_use ? payload.limited_data_use : false - } - ] - } - - // https://business-api.tiktok.com/portal/docs?id=1771101303285761 - return request('https://business-api.tiktok.com/open_api/v1.3/event/track/', { - method: 'post', - json: requestJson - }) -} - -function validateRequestUser(payload: Payload) { - const phone_numbers = formatPhones(payload.phone_number) - const emails = formatEmails(payload.email) - const userIds = formatUserIds(payload.external_id) - - let payloadUrl, urlTtclid - if (payload.url) { - try { - payloadUrl = new URL(payload.url) - } catch (error) { - // invalid url - } - } - - if (payloadUrl) urlTtclid = payloadUrl.searchParams.get('ttclid') - - const requestUser: TikTokConversionsUser = { - external_id: userIds, - phone: phone_numbers, - email: emails, - first_name: formatString(payload.first_name), - last_name: formatString(payload.last_name), - city: formatAddress(payload.address?.city), - state: formatAddress(payload.address?.state), - country: formatAddress(payload.address?.country), - zip_code: formatString(payload.address?.zip_code) - } - - if (payload.ttclid || urlTtclid) { - requestUser.ttclid = urlTtclid || payload.ttclid - } - - if (payload.lead_id) { - requestUser.lead_id = payload.lead_id - } - - if (payload.ttp) { - requestUser.ttp = payload.ttp - } - - if (payload.ip) { - requestUser.ip = payload.ip - } - - if (payload.user_agent) { - requestUser.user_agent = payload.user_agent - } - - if (payload.locale) { - requestUser.locale = payload.locale - } - - return requestUser -} - -function validateRequestProperties(payload: Payload) { - const requestProperties: TikTokConversionsProperties = { - contents: [] - } - - if (payload.contents) { - payload.contents.forEach((content) => { - const contentObj = { - price: content.price ? content.price : undefined, - quantity: content.quantity ? content.quantity : undefined, - content_category: content.content_category ? content.content_category : undefined, - content_id: content.content_id ? content.content_id : undefined, - content_name: content.content_name ? content.content_name : undefined, - brand: content.brand ? content.brand : undefined - } - requestProperties.contents.push(contentObj) - }) - } - - if (payload.content_type) { - requestProperties.content_type = payload.content_type - } - - if (payload.currency) { - requestProperties.currency = payload.currency - } - - if (payload.value || payload.value === 0) { - requestProperties.value = payload.value - } - - if (payload.query) { - requestProperties.query = payload.query - } - - if (payload.description) { - requestProperties.description = payload.description - } - - if (payload.order_id) { - requestProperties.order_id = payload.order_id - } - - if (payload.shop_id) { - requestProperties.shop_id = payload.shop_id - } - - return requestProperties -} - -function validateRequestPage(payload: Payload) { - const requestPage: TikTokConversionsPage = {} - - if (payload.url) { - requestPage.url = payload.url - } - - if (payload.referrer) { - requestPage.referrer = payload.referrer - } - - return requestPage -}