From 83fe2d8bc4ac830b025bec4f18b36c45fbf9eb22 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 1 Aug 2025 20:14:19 +0100 Subject: [PATCH 1/6] STRATCONN-6081 - [TikTok Conversions] - CRM support --- .../__tests__/formatter.test.ts | 8 +- .../__tests__/index.test.ts | 2 - .../tiktok-conversions/common_fields.ts | 593 +++++++++++++++++- .../tiktok-conversions/formatter.ts | 47 +- .../reportWebEvent/generated-types.ts | 223 +++++++ .../reportWebEvent/index.ts | 7 +- .../destinations/tiktok-conversions/types.ts | 89 ++- .../destinations/tiktok-conversions/utils.ts | 259 +++++--- 8 files changed, 1099 insertions(+), 129 deletions(-) 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..9d0dbd00ee6 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 @@ -4,7 +4,7 @@ describe('Tiktok Conversions Formatter', () => { describe('formatEmails', () => { it('should hash and encode email addresses', () => { const emails = ['bugsbunny@warnerbros.com', 'daffyduck@warnerbros.com'] - const result = formatEmails(emails, { 'smart-hashing': true }) + const result = formatEmails(emails) expect(result).toEqual([ '67e28cdcc3e845d3a4da05ca9fe5ddb7320a83b4cc2167f0555a3b04f63511e3', '2d2fb2388f17f86affee71d632978425a3037fa8eed5b3f2baaa458c80d495ed' @@ -16,7 +16,7 @@ describe('Tiktok Conversions Formatter', () => { '67e28cdcc3e845d3a4da05ca9fe5ddb7320a83b4cc2167f0555a3b04f63511e3', '2d2fb2388f17f86affee71d632978425a3037fa8eed5b3f2baaa458c80d495ed' ] - const result = formatEmails(emails, { 'smart-hashing': true }) + const result = formatEmails(emails) expect(result).toEqual(emails) }) }) @@ -24,7 +24,7 @@ describe('Tiktok Conversions Formatter', () => { describe('formatPhones', () => { it('should hash and encode phone numbers', () => { const phones = ['12345678901234', '98765432109876'] - const result = formatPhones(phones, { 'smart-hashing': true }) + const result = formatPhones(phones) expect(result).toEqual([ '00a63bd0437d6ca0fa7b95c00ab2e7c020faa71440e5246750d6b517689e6777', 'ce7b12d132a021393e793d21d6e8e673ac06042922b73cc10f2d7db597657a4a' @@ -36,7 +36,7 @@ describe('Tiktok Conversions Formatter', () => { 'c17c025fb9ed44eae8a9d5c9df0312af5c6161bd79bd669692364fc5ecaf108a', '5a1b78d720b151af8e69fde486784c1c279996813d20d23badbcce1e1037ee91' ] - const result = formatPhones(phones, { 'smart-hashing': true }) + const result = formatPhones(phones) expect(result).toEqual(phones) }) }) 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..6e19e4753b0 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: diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts index c147678a12b..adc60f8e484 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts @@ -1,6 +1,32 @@ import { InputField } from '@segment/actions-core' export const commonFields: 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 auto events.', + choices: [ + { value: 'Travel Fields', label: 'travel_fields' }, + { value: 'Auto Fields', label: 'auto_fields' } + ] + }, event: { label: 'Event Name', type: 'string', @@ -36,8 +62,7 @@ export const commonFields: Record = { then: { '@path': '$.properties.phone' }, else: { '@path': '$.context.traits.phone' } } - }, - category: 'hashedPII' + } }, email: { label: 'Email', @@ -51,8 +76,7 @@ export const commonFields: Record = { then: { '@path': '$.properties.email' }, else: { '@path': '$.context.traits.email' } } - }, - category: 'hashedPII' + } }, first_name: { label: 'First Name', @@ -65,8 +89,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 +102,7 @@ export const commonFields: Record = { then: { '@path': '$.properties.last_name' }, else: { '@path': '$.context.traits.last_name' } } - }, - category: 'hashedPII' + } }, address: { label: 'Address', @@ -101,8 +123,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 +190,7 @@ export const commonFields: Record = { then: { '@path': '$.userId' }, else: { '@path': '$.anonymousId' } } - }, - category: 'hashedPII' + } }, ttclid: { label: 'TikTok Click ID', @@ -251,7 +271,6 @@ export const commonFields: Record = { type: 'object', multiple: true, description: 'Related item details for the event.', - defaultObjectUI: 'keyvalue', properties: { price: { label: 'Price', @@ -345,3 +364,551 @@ export const commonFields: Record = { '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.' } } + +export const newFields: Record = { + content_ids: { + label: 'Content IDs', + description: 'Product IDs associated with the event, such as SKUs.', + type: 'string', + multiple: true, + default: { + '@path': '$.properties.content_ids' // TODO: check multiple value mapping + } + }, + 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.' } + ] + }, + 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' + } + }, + 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. Used with the Search event.', + default: { + '@path': '$.properties.search_string' + } + }, + 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' } + }, + depends_on: { + conditions: [ + { + fieldKey: 'event_source', + operator: 'is', + value: 'crm' + } + ] + } + } +} + +export const travelFields: 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.', + 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_airiport: { + 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.', + 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: [ + // TODO: have choices & default mapping? + { 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: 'number', + 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 suugestions for this user. This parameter is not applicable for the Search event.', + 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' // TODO: confirm multiple value mapping + }, + 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' // TODO: confirm multiple value mapping + }, + 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' // TODO: confirm multiple value mapping + } + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'event_source', + operator: 'is', + value: 'web' + }, + { + fieldKey: 'event_spec_type', + operator: 'is', + value: 'travel_fields' + } + ] + } +} + +export const autoFields: InputField = { + label: 'Auto 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).' + }, + 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.' + }, + 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.' + }, + 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.' + } + }, + 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: 'auto_fields' + } + ] + } +} diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts b/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts index 4390d7bcf4b..6d26ad7e2d2 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts @@ -1,4 +1,7 @@ -import { processHashing } from '../../lib/hashing-utils' +import { createHash } from 'crypto' + +const isHashedInformation = (information: string): boolean => new RegExp(/[0-9abcdef]{64}/gi).test(information) + /** * Convert emails to lower case, and hash in SHA256. */ @@ -6,7 +9,14 @@ export const formatEmails = (email_addresses: string[] | undefined): string[] => const result: string[] = [] if (email_addresses) { email_addresses.forEach((email: string) => { - result.push(hashAndEncode(email.toLowerCase())) + let resolvedEmail + if (isHashedInformation(email)) { + resolvedEmail = email + } else { + resolvedEmail = hashAndEncode(email.toLowerCase()) + } + + result.push(resolvedEmail) }) } return result @@ -22,20 +32,22 @@ export const formatPhones = (phone_numbers: string[] | undefined): string[] => { if (!phone_numbers) return result phone_numbers.forEach((phone: string) => { + if (isHashedInformation(phone)) { + result.push(phone) + return + } + + const validatedPhone = phone.match(/[0-9]{0,14}/g) + if (validatedPhone === null) { + throw new Error(`${phone} is not a valid E.164 phone number.`) + } + // Remove spaces and non-digits; append + to the beginning + const formattedPhone = `+${phone.replace(/[^0-9]/g, '')}` // Limit length to 15 characters - result.push(hashAndEncode(phone, cleanPhoneNumber)) + result.push(hashAndEncode(formattedPhone.substring(0, 15))) }) - return result -} -const cleanPhoneNumber = (phone: string): string => { - const validatedPhone = phone.match(/[0-9]{0,14}/g) - if (validatedPhone === null) { - throw new Error(`${phone} is not a valid E.164 phone number.`) - } - // Remove spaces and non-digits; append + to the beginning - const formattedPhone = `+${phone.replace(/[^0-9]/g, '')}` - return formattedPhone.substring(0, 15) + return result } /** @@ -55,7 +67,10 @@ export function formatUserIds(userIds: string[] | undefined): string[] { export function formatString(str: string | undefined | null): string | undefined { if (!str) return '' - return hashAndEncode(str.replace(/\s/g, '').toLowerCase()) + if (!isHashedInformation(str)) { + str = hashAndEncode(str.replace(/\s/g, '').toLowerCase()) + } + return str } export function formatAddress(address: string | undefined | null): string | undefined { @@ -63,6 +78,6 @@ export function formatAddress(address: string | undefined | null): string | unde return address.replace(/[^A-Za-z0-9]/g, '').toLowerCase() } -function hashAndEncode(property: string, cleaningFunction?: (value: string) => string): string { - return processHashing(property, 'sha256', 'hex', cleaningFunction) +function hashAndEncode(property: string) { + return createHash('sha256').update(property).digest('hex') } 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..e0728ec4a25 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 auto 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. */ @@ -151,4 +159,219 @@ 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 + /** + * Product IDs associated with the event, such as SKUs. + */ + content_ids?: string[] + /** + * Category of the delivery. + */ + delivery_category?: string + /** + * Number of items when checkout was initiated. Used with the InitiateCheckout event. + */ + num_items?: number + /** + * 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. 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. + */ + autoFields?: { + /** + * 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. + */ + travelFields?: { + /** + * 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. + */ + 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_airiport?: 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. + */ + 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 suugestions for this user. This parameter is not applicable for the Search event. + */ + 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..2af64247dcc 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts @@ -1,7 +1,7 @@ import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { commonFields } from '../common_fields' +import { commonFields, newFields, autoFields, travelFields } from '../common_fields' import { performWebEvent } from '../utils' const action: ActionDefinition = { @@ -9,7 +9,10 @@ const action: ActionDefinition = { 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 + ...commonFields, + ...newFields, + autoFields, + travelFields }, perform: (request, { payload, settings }) => { return performWebEvent(request, settings, payload) diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/types.ts b/packages/destination-actions/src/destinations/tiktok-conversions/types.ts index a2919120748..c4b8cd76f7f 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/types.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/types.ts @@ -1,32 +1,32 @@ -export interface TikTokConversionsRequest { +export interface TTJSON { event_source: string event_source_id: string partner_name: string test_event_code?: string - data: TikTokConversionsData[] + data: TTDataItem[] } - -export interface TikTokConversionsData { +interface TTDataItem { event: string event_time: number event_id?: string - user: TikTokConversionsUser - properties: TikTokConversionsProperties - page?: TikTokConversionsPage + 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 TikTokConversionsPage { - url?: string - referrer?: string -} - -export interface TikTokConversionsUser { +export interface TTUser { external_id: string[] phone: string[] email: string[] ttp?: string - lead_id?: string ip?: string user_agent?: string locale?: string @@ -39,8 +39,8 @@ export interface TikTokConversionsUser { ttclid?: string } -export interface TikTokConversionsProperties { - contents: TikTokConversionsContent[] +export interface TTBaseProps { + contents: TTContentItem[] content_type?: string currency?: string value?: number @@ -48,9 +48,64 @@ export interface TikTokConversionsProperties { 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 } -export interface TikTokConversionsContent { +interface TTContentItem { price?: number quantity?: number content_category?: string diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts b/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts index f1f6d28ce6b..3b0912fbfeb 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts @@ -3,45 +3,66 @@ import { Settings } from './generated-types' import { Payload } from './reportWebEvent/generated-types' import { formatEmails, formatPhones, formatUserIds, formatString, formatAddress } from './formatter' import { - TikTokConversionsPage, - TikTokConversionsProperties, - TikTokConversionsRequest, - TikTokConversionsUser + TTJSON, + TTAutoProps, + TTBaseProps, + TTTravelProps, + TTUser } from './types' export function performWebEvent(request: RequestClient, settings: Settings, payload: Payload) { - const requestUser = validateRequestUser(payload) - const requestProperties = validateRequestProperties(payload) - const requestPage = validateRequestPage(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 requestJson: TikTokConversionsRequest = { - event_source: 'web', + const user = getUser(payload) + const properties = getProps(payload) + const event_source = payload.event_source ?? 'web' + + const requestJson: TTJSON = { + event_source, event_source_id: settings.pixelCode, partner_name: 'Segment', - test_event_code: payload.test_event_code ? payload.test_event_code : undefined, + test_event_code: test_event_code ? test_event_code : undefined, data: [ { - event: payload.event, + 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 + event_id: event_id ? `${event_id}` : undefined, + user, + properties: { + ...properties, + ...(event_spec_type === 'travel' && event_source === 'web' ? getTravelProps(payload) : {}), + ...(event_spec_type === 'auto' && event_source === 'web' ? getAutoProps(payload) : {}) + }, + ...((url || referrer) ? { page: { ...(url && { url }), ...(referrer && { referrer }) } } : {}), + ...(event_source === 'crm' && (lead_id || lead_event_source) + ? { lead: { ...(lead_id && { lead_id }), ...(lead_event_source && { lead_event_source }) } } + : {} + ), + limited_data_use: typeof limited_data_use === 'boolean' ? 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) { +function getUser(payload: Payload): TTUser { const phone_numbers = formatPhones(payload.phone_number) const emails = formatEmails(payload.email) const userIds = formatUserIds(payload.external_id) @@ -57,7 +78,7 @@ function validateRequestUser(payload: Payload) { if (payloadUrl) urlTtclid = payloadUrl.searchParams.get('ttclid') - const requestUser: TikTokConversionsUser = { + const requestUser: TTUser = { external_id: userIds, phone: phone_numbers, email: emails, @@ -73,10 +94,6 @@ function validateRequestUser(payload: Payload) { requestUser.ttclid = urlTtclid || payload.ttclid } - if (payload.lead_id) { - requestUser.lead_id = payload.lead_id - } - if (payload.ttp) { requestUser.ttp = payload.ttp } @@ -96,66 +113,158 @@ function validateRequestUser(payload: Payload) { 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 +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: TTProps = { + 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 }) } - if (payload.value || payload.value === 0) { - requestProperties.value = payload.value - } - - if (payload.query) { - requestProperties.query = payload.query - } + return requestProperties +} - if (payload.description) { - requestProperties.description = payload.description - } +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_airiport, + destination_ids, + departing_arrival_date, + returning_arrival_date, + travel_class, + user_score, + preferred_num_stops, + travel_start, + travel_end, + suggested_destinations, + } = payload?.travelFields ?? {} - if (payload.order_id) { - requestProperties.order_id = payload.order_id - } - - if (payload.shop_id) { - requestProperties.shop_id = payload.shop_id + 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_airiport !== undefined && { destination_airiport }), + ...(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 validateRequestPage(payload: Payload) { - const requestPage: TikTokConversionsPage = {} +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 ?? {} - if (payload.url) { - requestPage.url = payload.url - } - - if (payload.referrer) { - requestPage.referrer = payload.referrer - } + 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 && mileage_value !== undefined && { + mileage: { + unit: mileage_unit, + value: mileage_value + } + }), + ...(preferred_price_range_min !== undefined && preferred_price_range_max !== undefined && { + preferred_price_range: [preferred_price_range_min, preferred_price_range_max] + }) + } - return requestPage -} + return requestProperties +} \ No newline at end of file From da02a3bf9ef414e15c4b0d2728a3462d96d8476b Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 1 Aug 2025 20:27:12 +0100 Subject: [PATCH 2/6] fixing stuff --- .../__tests__/formatter.test.ts | 8 ++-- .../tiktok-conversions/formatter.ts | 47 +++++++------------ 2 files changed, 20 insertions(+), 35 deletions(-) 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 9d0dbd00ee6..2f96103d6b3 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 @@ -4,7 +4,7 @@ describe('Tiktok Conversions Formatter', () => { describe('formatEmails', () => { it('should hash and encode email addresses', () => { const emails = ['bugsbunny@warnerbros.com', 'daffyduck@warnerbros.com'] - const result = formatEmails(emails) + const result = formatEmails(emails, { 'smart-hashing': true }) expect(result).toEqual([ '67e28cdcc3e845d3a4da05ca9fe5ddb7320a83b4cc2167f0555a3b04f63511e3', '2d2fb2388f17f86affee71d632978425a3037fa8eed5b3f2baaa458c80d495ed' @@ -16,7 +16,7 @@ describe('Tiktok Conversions Formatter', () => { '67e28cdcc3e845d3a4da05ca9fe5ddb7320a83b4cc2167f0555a3b04f63511e3', '2d2fb2388f17f86affee71d632978425a3037fa8eed5b3f2baaa458c80d495ed' ] - const result = formatEmails(emails) + const result = formatEmails(emails, { 'smart-hashing': true }) expect(result).toEqual(emails) }) }) @@ -24,7 +24,7 @@ describe('Tiktok Conversions Formatter', () => { describe('formatPhones', () => { it('should hash and encode phone numbers', () => { const phones = ['12345678901234', '98765432109876'] - const result = formatPhones(phones) + const result = formatPhones(phones, { 'smart-hashing': true }) expect(result).toEqual([ '00a63bd0437d6ca0fa7b95c00ab2e7c020faa71440e5246750d6b517689e6777', 'ce7b12d132a021393e793d21d6e8e673ac06042922b73cc10f2d7db597657a4a' @@ -36,7 +36,7 @@ describe('Tiktok Conversions Formatter', () => { 'c17c025fb9ed44eae8a9d5c9df0312af5c6161bd79bd669692364fc5ecaf108a', '5a1b78d720b151af8e69fde486784c1c279996813d20d23badbcce1e1037ee91' ] - const result = formatPhones(phones) + const result = formatPhones(phones, { 'smart-hashing': true }) expect(result).toEqual(phones) }) }) diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts b/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts index 6d26ad7e2d2..4390d7bcf4b 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/formatter.ts @@ -1,7 +1,4 @@ -import { createHash } from 'crypto' - -const isHashedInformation = (information: string): boolean => new RegExp(/[0-9abcdef]{64}/gi).test(information) - +import { processHashing } from '../../lib/hashing-utils' /** * Convert emails to lower case, and hash in SHA256. */ @@ -9,14 +6,7 @@ export const formatEmails = (email_addresses: string[] | undefined): string[] => const result: string[] = [] if (email_addresses) { email_addresses.forEach((email: string) => { - let resolvedEmail - if (isHashedInformation(email)) { - resolvedEmail = email - } else { - resolvedEmail = hashAndEncode(email.toLowerCase()) - } - - result.push(resolvedEmail) + result.push(hashAndEncode(email.toLowerCase())) }) } return result @@ -32,24 +22,22 @@ export const formatPhones = (phone_numbers: string[] | undefined): string[] => { if (!phone_numbers) return result phone_numbers.forEach((phone: string) => { - if (isHashedInformation(phone)) { - result.push(phone) - return - } - - const validatedPhone = phone.match(/[0-9]{0,14}/g) - if (validatedPhone === null) { - throw new Error(`${phone} is not a valid E.164 phone number.`) - } - // Remove spaces and non-digits; append + to the beginning - const formattedPhone = `+${phone.replace(/[^0-9]/g, '')}` // Limit length to 15 characters - result.push(hashAndEncode(formattedPhone.substring(0, 15))) + result.push(hashAndEncode(phone, cleanPhoneNumber)) }) - return result } +const cleanPhoneNumber = (phone: string): string => { + const validatedPhone = phone.match(/[0-9]{0,14}/g) + if (validatedPhone === null) { + throw new Error(`${phone} is not a valid E.164 phone number.`) + } + // Remove spaces and non-digits; append + to the beginning + const formattedPhone = `+${phone.replace(/[^0-9]/g, '')}` + return formattedPhone.substring(0, 15) +} + /** * * @param userId @@ -67,10 +55,7 @@ export function formatUserIds(userIds: string[] | undefined): string[] { export function formatString(str: string | undefined | null): string | undefined { if (!str) return '' - if (!isHashedInformation(str)) { - str = hashAndEncode(str.replace(/\s/g, '').toLowerCase()) - } - return str + return hashAndEncode(str.replace(/\s/g, '').toLowerCase()) } export function formatAddress(address: string | undefined | null): string | undefined { @@ -78,6 +63,6 @@ export function formatAddress(address: string | undefined | null): string | unde return address.replace(/[^A-Za-z0-9]/g, '').toLowerCase() } -function hashAndEncode(property: string) { - return createHash('sha256').update(property).digest('hex') +function hashAndEncode(property: string, cleaningFunction?: (value: string) => string): string { + return processHashing(property, 'sha256', 'hex', cleaningFunction) } From ea62ef2652438b84b24b713268033b4f446957aa Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 1 Aug 2025 20:40:25 +0100 Subject: [PATCH 3/6] refactor --- .../{ => reportWebEvent}/common_fields.ts | 466 +----------------- .../reportWebEvent/index.ts | 10 +- .../reportWebEvent/travel_fields.ts | 222 +++++++++ .../reportWebEvent/vehicle_fields.ts | 244 +++++++++ .../destinations/tiktok-conversions/utils.ts | 66 +-- 5 files changed, 506 insertions(+), 502 deletions(-) rename packages/destination-actions/src/destinations/tiktok-conversions/{ => reportWebEvent}/common_fields.ts (51%) create mode 100644 packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/travel_fields.ts create mode 100644 packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/common_fields.ts similarity index 51% rename from packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts rename to packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/common_fields.ts index adc60f8e484..879271d78e7 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/common_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/common_fields.ts @@ -365,7 +365,7 @@ export const commonFields: Record = { } } -export const newFields: Record = { +export const new_fields: Record = { content_ids: { label: 'Content IDs', description: 'Product IDs associated with the event, such as SKUs.', @@ -448,467 +448,3 @@ export const newFields: Record = { } } } - -export const travelFields: 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.', - 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_airiport: { - 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.', - 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: [ - // TODO: have choices & default mapping? - { 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: 'number', - 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 suugestions for this user. This parameter is not applicable for the Search event.', - 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' // TODO: confirm multiple value mapping - }, - 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' // TODO: confirm multiple value mapping - }, - 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' // TODO: confirm multiple value mapping - } - }, - depends_on: { - match: 'all', - conditions: [ - { - fieldKey: 'event_source', - operator: 'is', - value: 'web' - }, - { - fieldKey: 'event_spec_type', - operator: 'is', - value: 'travel_fields' - } - ] - } -} - -export const autoFields: InputField = { - label: 'Auto 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).' - }, - 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.' - }, - 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.' - }, - 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.' - } - }, - 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: 'auto_fields' - } - ] - } -} 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 2af64247dcc..37e3e956962 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts @@ -1,7 +1,9 @@ import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { commonFields, newFields, autoFields, travelFields } from '../common_fields' +import { commonFields, new_fields } from './common_fields' +import { travel_fields } from './travel_fields' +import { vehicle_fields } from './vehicle_fields' import { performWebEvent } from '../utils' const action: ActionDefinition = { @@ -10,9 +12,9 @@ const action: ActionDefinition = { 'Report Web events directly to TikTok. Data shared can power TikTok solutions like dynamic product ads, custom targeting, campaign optimization and attribution.', fields: { ...commonFields, - ...newFields, - autoFields, - travelFields + ...new_fields, + vehicle_fields, + travel_fields }, perform: (request, { payload, settings }) => { return performWebEvent(request, settings, payload) diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/travel_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/travel_fields.ts new file mode 100644 index 00000000000..6717cf6ff66 --- /dev/null +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/travel_fields.ts @@ -0,0 +1,222 @@ +import { InputField } from '@segment/actions-core' + +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.', + 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_airiport: { + 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.', + 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: [ + // TODO: have choices & default mapping? + { 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: 'number', + 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 suugestions for this user. This parameter is not applicable for the Search event.', + 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' // TODO: confirm multiple value mapping + }, + 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' // TODO: confirm multiple value mapping + }, + 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' // TODO: confirm multiple value mapping + } + }, + 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/vehicle_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts new file mode 100644 index 00000000000..3bab2943365 --- /dev/null +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts @@ -0,0 +1,244 @@ +import { InputField } from '@segment/actions-core' + +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).' + }, + 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.' + }, + 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.' + }, + 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.' + } + }, + 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: 'auto_fields' + } + ] + } +} diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts b/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts index 3b0912fbfeb..0804297dd32 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts @@ -63,14 +63,29 @@ export function performWebEvent(request: RequestClient, settings: Settings, payl } function getUser(payload: Payload): TTUser { - const phone_numbers = formatPhones(payload.phone_number) - const emails = formatEmails(payload.email) - const userIds = formatUserIds(payload.external_id) + 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 (payload.url) { + if (url) { try { - payloadUrl = new URL(payload.url) + payloadUrl = new URL(url) } catch (error) { // invalid url } @@ -82,34 +97,19 @@ function getUser(payload: Payload): TTUser { 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.ttp) { - requestUser.ttp = payload.ttp + 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 } : {}) } - - 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 } @@ -130,7 +130,7 @@ function getProps(payload: Payload): TTBaseProps { contents } = payload - const requestProperties: TTProps = { + const requestProperties: TTBaseProps = { contents: contents ? contents.map(({ price, quantity, content_category, content_id, content_name, brand }) => ({ price: price ?? undefined, From e9c4e0863b030028d463241759e18bfa561db048 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Sat, 2 Aug 2025 14:25:50 +0100 Subject: [PATCH 4/6] refactor --- .../__tests__/formatter.test.ts | 2 +- .../destinations/tiktok-conversions/index.ts | 1 - .../reportWebEvent/common_fields.ts | 17 +++++++++-------- .../reportWebEvent/constants.ts | 4 ++++ .../{ => reportWebEvent}/formatter.ts | 2 +- .../tiktok-conversions/reportWebEvent/index.ts | 8 ++++---- .../reportWebEvent/travel_fields.ts | 5 +++-- .../{ => reportWebEvent}/types.ts | 0 .../{ => reportWebEvent}/utils.ts | 17 +++++++++-------- .../reportWebEvent/vehicle_fields.ts | 5 +++-- 10 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/constants.ts rename packages/destination-actions/src/destinations/tiktok-conversions/{ => reportWebEvent}/formatter.ts (97%) rename packages/destination-actions/src/destinations/tiktok-conversions/{ => reportWebEvent}/types.ts (100%) rename packages/destination-actions/src/destinations/tiktok-conversions/{ => reportWebEvent}/utils.ts (93%) 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/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/common_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/common_fields.ts index 879271d78e7..a38f6aa2d77 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/common_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/common_fields.ts @@ -1,19 +1,20 @@ 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', + default: WEB, choices: [ { - value: 'web', + value: WEB, label: 'The events took place on your website and are measured by a Pixel Code.' }, { - value: 'crm', + value: CRM, label: 'The lead events took place on a CRM system and are tracked by a CRM Event Set ID.' } ] @@ -21,10 +22,10 @@ export const commonFields: Record = { event_spec_type: { label: 'Additional Fields', type: 'string', - description: 'Include fields for travel or auto events.', + description: 'Include fields for travel or vehicle events.', choices: [ - { value: 'Travel Fields', label: 'travel_fields' }, - { value: 'Auto Fields', label: 'auto_fields' } + { value: 'Travel Fields', label: TRAVEL_FIELDS }, + { value: 'Vehicle Fields', label: VEHICLE_FIELDS } ] }, event: { @@ -442,7 +443,7 @@ export const new_fields: Record = { { fieldKey: 'event_source', operator: 'is', - value: 'crm' + value: CRM } ] } 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/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/index.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts index 37e3e956962..92b270cbde2 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/index.ts @@ -1,23 +1,23 @@ import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { commonFields, new_fields } from './common_fields' +import { common_fields, new_fields } from './common_fields' import { travel_fields } from './travel_fields' import { vehicle_fields } from './vehicle_fields' -import { performWebEvent } from '../utils' +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, ...new_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/travel_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/travel_fields.ts index 6717cf6ff66..73a756f2317 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/travel_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/travel_fields.ts @@ -1,4 +1,5 @@ import { InputField } from '@segment/actions-core' +import { TRAVEL_FIELDS, WEB } from './constants' export const travel_fields: InputField = { label: 'Travel Fields', @@ -210,12 +211,12 @@ export const travel_fields: InputField = { { fieldKey: 'event_source', operator: 'is', - value: 'web' + value: WEB }, { fieldKey: 'event_spec_type', operator: 'is', - value: 'travel_fields' + value: TRAVEL_FIELDS } ] } diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/types.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/types.ts similarity index 100% rename from packages/destination-actions/src/destinations/tiktok-conversions/types.ts rename to packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/types.ts diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/utils.ts similarity index 93% rename from packages/destination-actions/src/destinations/tiktok-conversions/utils.ts rename to packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/utils.ts index 0804297dd32..66256546c4e 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/utils.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/utils.ts @@ -1,7 +1,8 @@ import { RequestClient } from '@segment/actions-core' -import { Settings } from './generated-types' -import { Payload } from './reportWebEvent/generated-types' +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, @@ -10,7 +11,7 @@ import { TTUser } from './types' -export function performWebEvent(request: RequestClient, settings: Settings, payload: Payload) { +export function send(request: RequestClient, settings: Settings, payload: Payload) { const { event, event_id, @@ -26,7 +27,7 @@ export function performWebEvent(request: RequestClient, settings: Settings, payl const user = getUser(payload) const properties = getProps(payload) - const event_source = payload.event_source ?? 'web' + const event_source = payload.event_source ?? WEB const requestJson: TTJSON = { event_source, @@ -43,11 +44,11 @@ export function performWebEvent(request: RequestClient, settings: Settings, payl user, properties: { ...properties, - ...(event_spec_type === 'travel' && event_source === 'web' ? getTravelProps(payload) : {}), - ...(event_spec_type === 'auto' && event_source === 'web' ? getAutoProps(payload) : {}) + ...(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' && (lead_id || lead_event_source) + ...(event_source === CRM && (lead_id || lead_event_source) ? { lead: { ...(lead_id && { lead_id }), ...(lead_event_source && { lead_event_source }) } } : {} ), @@ -109,7 +110,7 @@ function getUser(payload: Payload): TTUser { ...(user_agent ? { user_agent } : {}), ...(locale ? { locale } : {}) } - + return requestUser } diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts index 3bab2943365..82af8813681 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts @@ -1,4 +1,5 @@ import { InputField } from '@segment/actions-core' +import { VEHICLE_FIELDS, WEB } from './constants' export const vehicle_fields: InputField = { label: 'Vehicle Fields', @@ -232,12 +233,12 @@ export const vehicle_fields: InputField = { { fieldKey: 'event_source', operator: 'is', - value: 'web' + value: WEB }, { fieldKey: 'event_spec_type', operator: 'is', - value: 'auto_fields' + value: VEHICLE_FIELDS } ] } From 565869fcf6bdfdcd10828e64cdc29263a98b9815 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 7 Aug 2025 14:58:55 +0100 Subject: [PATCH 5/6] Changes after meeting with Jae --- .../__tests__/index.test.ts | 79 ++++++++++--------- .../{ => fields}/common_fields.ts | 77 +++++------------- .../reportWebEvent/fields/crm_fields.ts | 49 ++++++++++++ .../{ => fields}/travel_fields.ts | 19 +++-- .../{ => fields}/vehicle_fields.ts | 32 ++++++-- .../reportWebEvent/generated-types.ts | 36 ++++----- .../reportWebEvent/index.ts | 9 ++- .../reportWebEvent/types.ts | 2 +- .../reportWebEvent/utils.ts | 22 +++--- 9 files changed, 181 insertions(+), 144 deletions(-) rename packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/{ => fields}/common_fields.ts (88%) create mode 100644 packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/crm_fields.ts rename packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/{ => fields}/travel_fields.ts (88%) rename packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/{ => fields}/vehicle_fields.ts (90%) 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 6e19e4753b0..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 @@ -571,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', @@ -651,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/reportWebEvent/common_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/common_fields.ts similarity index 88% rename from packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/common_fields.ts rename to packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/common_fields.ts index a38f6aa2d77..db8988fdebe 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/common_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/common_fields.ts @@ -1,5 +1,5 @@ import { InputField } from '@segment/actions-core' -import { VEHICLE_FIELDS, TRAVEL_FIELDS, WEB, CRM } from './constants' +import { VEHICLE_FIELDS, TRAVEL_FIELDS, WEB, CRM } from '../constants' export const common_fields: Record = { event_source: { @@ -219,13 +219,6 @@ export const common_fields: 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: @@ -305,6 +298,23 @@ export const common_fields: 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: @@ -363,18 +373,6 @@ export const common_fields: 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.' - } -} - -export const new_fields: Record = { - content_ids: { - label: 'Content IDs', - description: 'Product IDs associated with the event, such as SKUs.', - type: 'string', - multiple: true, - default: { - '@path': '$.properties.content_ids' // TODO: check multiple value mapping - } }, delivery_category: { label: 'Delivery Category', @@ -389,14 +387,6 @@ export const new_fields: Record = { { value: 'home_delivery', label: 'Home Delivery - Purchase is delivered to the customer.' } ] }, - 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' - } - }, predicted_ltv: { label: 'Prediected Lifetime Value', type: 'number', @@ -409,41 +399,16 @@ export const new_fields: Record = { search_string: { label: 'Search String', type: 'string', - description: 'The text string entered by the user for the search. Used with the Search event.', + description: 'The text string entered by the user for the search. Optionally used with the Search event.', default: { '@path': '$.properties.search_string' - } - }, - 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' } }, depends_on: { conditions: [ { - fieldKey: 'event_source', + fieldKey: 'event', operator: 'is', - value: CRM + 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..ca193b3d873 --- /dev/null +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/crm_fields.ts @@ -0,0 +1,49 @@ +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', + required: true + }, + 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/travel_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/travel_fields.ts similarity index 88% rename from packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/travel_fields.ts rename to packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/travel_fields.ts index 73a756f2317..8278697d962 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/travel_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/travel_fields.ts @@ -1,5 +1,5 @@ import { InputField } from '@segment/actions-core' -import { TRAVEL_FIELDS, WEB } from './constants' +import { TRAVEL_FIELDS, WEB } from '../constants' export const travel_fields: InputField = { label: 'Travel Fields', @@ -50,7 +50,7 @@ export const travel_fields: InputField = { }, suggested_hotels: { label: 'Suggested Hotels', - description: 'Suggested hotels.', + description: 'Suggested hotels. This can be a single string value or an array of string values.', type: 'string', multiple: true }, @@ -71,7 +71,7 @@ export const travel_fields: InputField = { type: 'string', description: 'Origin airport.' }, - destination_airiport: { + destination_airport: { label: 'Destination Airport', type: 'string', description: 'Destination airport.' @@ -79,7 +79,7 @@ export const travel_fields: InputField = { 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.', + '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 }, @@ -100,7 +100,6 @@ export const travel_fields: InputField = { type: 'string', description: 'Class of the flight ticket, must be: "eco", "prem", "bus", "first".', choices: [ - // TODO: have choices & default mapping? { value: 'eco', label: 'Economy' }, { value: 'prem', label: 'Premium' }, { value: 'bus', label: 'Bus' }, @@ -114,7 +113,7 @@ export const travel_fields: InputField = { }, preferred_num_stops: { label: 'Preferred Number of Stops', - type: 'number', + type: 'integer', description: 'The preferred number of stops the user is looking for. 0 for direct flight.' }, travel_start: { @@ -132,7 +131,7 @@ export const travel_fields: InputField = { suggested_destinations: { label: 'Suggested Destination IDs', description: - 'A list of IDs representing destination suugestions for this user. This parameter is not applicable for the Search event.', + '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 } @@ -163,7 +162,7 @@ export const travel_fields: InputField = { '@path': '$.properties.num_infants' }, suggested_hotels: { - '@path': '$.properties.suggested_hotels' // TODO: confirm multiple value mapping + '@path': '$.properties.suggested_hotels' // Confirmed this can be a single string or an array of strings }, departing_departure_date: { '@path': '$.properties.departing_departure_date' @@ -178,7 +177,7 @@ export const travel_fields: InputField = { '@path': '$.properties.destination_airiport' }, destination_ids: { - '@path': '$.properties.destination_ids' // TODO: confirm multiple value mapping + '@path': '$.properties.destination_ids' // Confirmed this can be a single string or an array of strings }, departing_arrival_date: { '@path': '$.properties.departing_arrival_date' @@ -202,7 +201,7 @@ export const travel_fields: InputField = { '@path': '$.properties.travel_end' }, suggested_destinations: { - '@path': '$.properties.suggested_destinations' // TODO: confirm multiple value mapping + '@path': '$.properties.suggested_destinations' // Confirmed this can be a single string or an array of strings } }, depends_on: { diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/vehicle_fields.ts similarity index 90% rename from packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts rename to packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/vehicle_fields.ts index 82af8813681..a60fdb0d577 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/vehicle_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/fields/vehicle_fields.ts @@ -1,5 +1,5 @@ import { InputField } from '@segment/actions-core' -import { VEHICLE_FIELDS, WEB } from './constants' +import { VEHICLE_FIELDS, WEB } from '../constants' export const vehicle_fields: InputField = { label: 'Vehicle Fields', @@ -46,7 +46,11 @@ export const vehicle_fields: InputField = { mileage_unit: { label: 'Mileage Unit', type: 'string', - description: 'Mileage unites in miles (MI) or kilometers (KM).' + 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', @@ -147,19 +151,37 @@ export const vehicle_fields: InputField = { viewcontent_type: { label: 'Soft Lead Landing Page', type: 'string', - description: 'Optional for ViewContent. Use viewcontent_type to differentiate between soft lead landing pages.' + 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.' + '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.' + '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: { 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 e0728ec4a25..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 @@ -6,7 +6,7 @@ export interface Payload { */ event_source?: string /** - * Include fields for travel or auto events. + * Include fields for travel or vehicle events. */ event_spec_type?: string /** @@ -78,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). */ @@ -131,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`. */ @@ -159,24 +163,16 @@ 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 - /** - * Product IDs associated with the event, such as SKUs. - */ - content_ids?: string[] /** * Category of the delivery. */ delivery_category?: string - /** - * Number of items when checkout was initiated. Used with the InitiateCheckout event. - */ - num_items?: number /** * 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. Used with the Search event. + * The text string entered by the user for the search. Optionally used with the Search event. */ search_string?: string /** @@ -195,7 +191,7 @@ export interface Payload { /** * Fields related to vehicle events. */ - autoFields?: { + vehicle_fields?: { /** * Postal code for the vehicle location. */ @@ -284,7 +280,7 @@ export interface Payload { /** * Fields related to travel events. */ - travelFields?: { + travel_fields?: { /** * Hotel city location. */ @@ -318,7 +314,7 @@ export interface Payload { */ num_infants?: number /** - * Suggested hotels. + * Suggested hotels. This can be a single string value or an array of string values. */ suggested_hotels?: string[] /** @@ -336,9 +332,9 @@ export interface Payload { /** * Destination airport. */ - destination_airiport?: string + 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. + * 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[] /** @@ -370,7 +366,7 @@ export interface Payload { */ travel_end?: string /** - * A list of IDs representing destination suugestions for this user. This parameter is not applicable for the Search event. + * 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 92b270cbde2..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,9 +1,10 @@ import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { common_fields, new_fields } from './common_fields' -import { travel_fields } from './travel_fields' -import { vehicle_fields } from './vehicle_fields' +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 = { @@ -12,7 +13,7 @@ const action: ActionDefinition = { 'Report Web events directly to TikTok. Data shared can power TikTok solutions like dynamic product ads, custom targeting, campaign optimization and attribution.', fields: { ...common_fields, - ...new_fields, + ...crm_fields, vehicle_fields, travel_fields }, diff --git a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/types.ts b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/types.ts index c4b8cd76f7f..aab571ca0f1 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/types.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/types.ts @@ -17,7 +17,7 @@ interface TTDataItem { } limited_data_use: boolean lead?: { - lead_id?: string + lead_id: string lead_event_source?: 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 index 66256546c4e..aeb239ccd9a 100644 --- a/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/utils.ts +++ b/packages/destination-actions/src/destinations/tiktok-conversions/reportWebEvent/utils.ts @@ -30,7 +30,7 @@ export function send(request: RequestClient, settings: Settings, payload: Payloa const event_source = payload.event_source ?? WEB const requestJson: TTJSON = { - event_source, + 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, @@ -48,10 +48,14 @@ export function send(request: RequestClient, settings: Settings, payload: Payloa ...(event_spec_type === VEHICLE_FIELDS && event_source === WEB ? getAutoProps(payload) : {}) }, ...((url || referrer) ? { page: { ...(url && { url }), ...(referrer && { referrer }) } } : {}), - ...(event_source === CRM && (lead_id || lead_event_source) - ? { lead: { ...(lead_id && { lead_id }), ...(lead_event_source && { lead_event_source }) } } - : {} - ), + ...(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 } ] @@ -173,7 +177,7 @@ function getTravelProps(payload: Payload): TTTravelProps { departing_departure_date, returning_departure_date, origin_airport, - destination_airiport, + destination_airport, destination_ids, departing_arrival_date, returning_arrival_date, @@ -198,7 +202,7 @@ function getTravelProps(payload: Payload): TTTravelProps { ...(departing_departure_date !== undefined && { departing_departure_date }), ...(returning_departure_date !== undefined && { returning_departure_date }), ...(origin_airport !== undefined && { origin_airport }), - ...(destination_airiport !== undefined && { destination_airiport }), + ...(destination_airport !== undefined && { destination_airport }), ...(destination_ids !== undefined && { destination_ids }), ...(departing_arrival_date !== undefined && { departing_arrival_date }), ...(returning_arrival_date !== undefined && { returning_arrival_date }), @@ -256,13 +260,13 @@ function getAutoProps(payload: Payload): TTAutoProps { ...(viewcontent_type !== undefined && { viewcontent_type }), ...(search_type !== undefined && { search_type }), ...(registration_type !== undefined && { registration_type }), - ...(mileage_unit !== undefined && mileage_value !== undefined && { + ...(mileage_unit !== undefined && typeof mileage_value === 'number' && { mileage: { unit: mileage_unit, value: mileage_value } }), - ...(preferred_price_range_min !== undefined && preferred_price_range_max !== undefined && { + ...(typeof preferred_price_range_min === 'number' && typeof preferred_price_range_max === 'number' && { preferred_price_range: [preferred_price_range_min, preferred_price_range_max] }) } From 8898fcfc0baf13755cba3d5220221f83bc87a99f Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 7 Aug 2025 15:00:37 +0100 Subject: [PATCH 6/6] Minor change for lead id --- .../tiktok-conversions/reportWebEvent/fields/crm_fields.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index ca193b3d873..03316ab0640 100644 --- 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 @@ -13,8 +13,7 @@ export const crm_fields: Record = { 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', - required: true + type: 'string' }, lead_event_source: { label: 'TikTok Lead Event Source',