diff --git a/README.md b/README.md index 469e8c2..b07d1af 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,46 @@ Find out more about Managed Components [here](https://blog.cloudflare.com/zaraz- `tid` Pixel Tag ID - The Pinterest Tag ID is the unique identifier of your Pinterest tag. [Learn more](https://help.pinterest.com/en/business/article/track-conversions-with-pinterest-tag). +--- + +## Fields Description + +Fields are properties that can/must be sent with certain events. + +### Required Fields + +#### Event Name `string` _required_ + +`event` - The name of the tracking event to be sent to Pinterest. + +### Optional Fields + +#### User Defined Event `string` _optional_ + +`ude` - Specify a custom event name for audience targeting purposes. Spaces in the event name will be removed. [Learn more](https://help.pinterest.com/en/business/article/add-event-codes). + +#### Partner Data Email `string` _optional_ + +`pdem` - Specifies the email address associated with the partner data, if applicable. + +#### Tag Manager `string` _optional_ + +`tm` - Indicates the Tag Manager used, defaults to 'pinterest-mc' if not specified. + +#### Lead Type `string` _optional_ + +`lead_type` - Describes the type of lead being tracked, such as 'Newsletter', 'Signup', etc. + +#### Video Title `string` _optional_ + +`video_title` - The title of the video for tracking specific video events. + +#### E-commerce Tracking `boolean` _optional_ + +`ecommerce` - Enables or disables the forwarding of Zaraz E-commerce API events to Pinterest. This includes events like Search, AddToCart, and Checkout. [Learn more](https://help.pinterest.com/en-gb/business/article/add-event-codes). + +--- + ## 📝 License Licensed under the [Apache License](./LICENSE). diff --git a/assets/pinterest.png b/assets/pinterest.png deleted file mode 100644 index 7ade056..0000000 Binary files a/assets/pinterest.png and /dev/null differ diff --git a/assets/pinterest.svg b/assets/pinterest.svg new file mode 100644 index 0000000..77c7e38 --- /dev/null +++ b/assets/pinterest.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/manifest.json b/manifest.json index 5002806..8c8698c 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "name": "Pinterest", "namespace": "pinterest", "description": "Pinterest Managed Component", - "icon": "assets/icon.svg", + "icon": "assets/pinterest.svg", "categories": ["Analytics"], "provides": ["events"], "allowCustomFields": true, diff --git a/src/index.test.ts b/src/index.test.ts index e50089e..0e535fb 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -13,34 +13,20 @@ describe('Pinterest MC sends correct request', () => { const baseHost = `${baseHostname}:${port}` const baseOrigin = `https://${baseHost}` const baseHref = `${baseOrigin}/` - const searchParams = new URLSearchParams() - const mockEvent = new Event('pagevisit', {}) as MCEvent - mockEvent.name = 'Pinterest Test' - mockEvent.payload = { timestamp: 1670409810, event: 'pagevisit', tid: 'xyz' } - mockEvent.client = { - url: { - href: baseHref, - origin: baseOrigin, - protocol: 'http:', - username: '', - password: '', - host: baseHost, - hostname: baseHostname, - port: port, - pathname: '/', - search: '', - searchParams: searchParams, - hash: '', + const mockEvent: MCEvent = { + payload: { timestamp: 1670409810, event: 'pageview', tid: 'xyz' }, + client: { + url: new URL(baseHref), + title: 'Zaraz "Test" /t Page', + timestamp: 1670409810, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', + language: 'en-GB', + referer: `${baseOrigin}/somewhere-else.html`, + ip: baseHostname, + emitter: 'browser', }, - title: 'Zaraz "Test" /t Page', - timestamp: 1670409810, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', - language: 'en-GB', - referer: `${baseOrigin}/somewhere-else.html`, - ip: baseHostname, - emitter: 'browser', } const settings: ComponentSettings = {} @@ -83,7 +69,7 @@ describe('Pinterest MC sends correct request', () => { 'pd[tm]': 'pinterest-mc', ed: '{"timestamp":1670409810,"event":"pagevisit"}', } - const requestUrl = getRequestUrl(rawRequestBody, mockEvent, settings) + const requestUrl = getRequestUrl(rawRequestBody) const requestUrlDecoded = decodeURI(requestUrl) const expectedUrl = `https://ct.pinterest.com/v3/?ad={"loc"%3A"https%3A%2F%2F127.0.0.1%3A1337%2F"%2C"ref"%3A"https%3A%2F%2F127.0.0.1%3A1337%2Fsomewhere-else.html"%2C"if"%3Afalse%2C"mh"%3A"2424edb5"}&cb=1671006315874&tid=xyz&event=pagevisit&pd[tm]=pinterest-mc&ed={"timestamp"%3A1670409810%2C"event"%3A"pagevisit"}` @@ -93,7 +79,7 @@ describe('Pinterest MC sends correct request', () => { it('Handler invokes fetch correctly', () => { const arr = [] - handler('pageview', mockEvent, settings, (...args) => { + handler(mockEvent, settings, 'pageview', (...args) => { arr.push(args) }) expect(arr.length).toBe(1) diff --git a/src/index.ts b/src/index.ts index 102e690..e56a75c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,82 @@ export type RequestBodyType = { ed?: string } +type EcommerceType = { + order_id: number | string + currency: string + revenue: number | string + total: number | string + value: number | string + quantity: number | string + products: Product[] | null + checkout_id: number | string + affiliation: string + shipping: number | string + tax: number | string + discount: number | string + coupon: string + creative: string + query: string + step: number | string + payment_type: string +} + +export type Product = { + product_id: number | string + sku: number | string + name: string + category: string + brand: string + price: number | string + quantity: number + variant: string + currency: string + value: number | string + position: number | string + coupon: number | string +} +const eventMappings: { [key: string]: string } = { + 'Product Added': 'addtocart', + 'Order Completed': 'checkout', + 'Products Searched': 'search', +} +function mapEcommerceEvent(eventName: string): string | undefined { + return eventMappings[eventName] || undefined +} +function mapEcommerceData( + ecommerce: EcommerceType +): Record | null { + const transformedProductData: Record = {} + if (!ecommerce) { + return null + } else { + ecommerce.products?.forEach((product, index) => + [ + 'product_id', + 'sku', + 'category', + 'name', + 'brand', + 'variant', + 'price', + ].forEach(prop => { + const key = `product_${prop}[${index}]` + transformedProductData[key] = + product[prop as keyof Product] || product.sku + }) + ) + } + + const ecommerceData = { + order_id: ecommerce.order_id, + currency: ecommerce.currency, + value: ecommerce.revenue || ecommerce.total || ecommerce.value, + order_quantity: ecommerce.quantity, + ...transformedProductData, + } + return ecommerceData +} + export const getRequestBody = ( eventType: string, event: MCEvent, @@ -44,8 +120,7 @@ export const getRequestBody = ( event: eventType, } - const { 'pd[em]': pdem, tid, ...cleanPayload } = payload - + const { pdem, tid, ecommerce, ...cleanPayload } = payload // pd - partner data if (pdem) { requestBody['pd[em]'] = pdem @@ -53,11 +128,17 @@ export const getRequestBody = ( requestBody['pd[tm]'] = payload.tm || 'pinterest-mc' + // match event types to Pinterest's default + + const ecommerceData = mapEcommerceData(ecommerce) + for (const key in ecommerceData) { + cleanPayload[key] = ecommerceData[key] + } + if (Object.keys(cleanPayload).length) { - // event data + // event data is created, note that it also holds the ecommerce parameters requestBody['ed'] = JSON.stringify(cleanPayload) } - return requestBody } @@ -75,54 +156,41 @@ export const sendRequest = (url: string, event: MCEvent) => { } export const handler = ( - eventType: string, event: MCEvent, settings: ComponentSettings, + ev: string, customSendRequest = sendRequest ) => { + const eventType = event.payload.ude || ev // ude is the "user defined event" field in case of eventType ='user-defined-event' const requestBody = getRequestBody(eventType, event, settings) const requestUrl = getRequestUrl(requestBody) customSendRequest(requestUrl, event) } export default async function (manager: Manager, settings: ComponentSettings) { - manager.addEventListener('pageview', event => { - handler('pagevisit', event, settings) + const events = [ + 'pageview', + 'lead', + 'signup', + 'watchvideo', + 'viewcategory', + 'custom', + 'addtocart', + 'checkout', + 'search', + 'user-defined-event', + ] + events.forEach(ev => { + manager.addEventListener(ev, event => { + handler(event, settings, ev) + }) }) - - manager.addEventListener('addtocart', event => { - handler('addtocart', event, settings) - }) - - manager.addEventListener('checkout', event => { - handler('checkout', event, settings) - }) - - manager.addEventListener('lead', event => { - handler('lead', event, settings) - }) - - manager.addEventListener('signup', event => { - handler('signup', event, settings) - }) - - manager.addEventListener('viewcategory', event => { - handler('viewcategory', event, settings) - }) - manager.addEventListener('watchvideo', event => { - handler('watchVideo', event, settings) - }) - - manager.addEventListener('custom', event => { - handler('custom', event, settings) - }) - - manager.addEventListener('search', event => { - handler('search', event, settings) - }) - - manager.addEventListener('userdefinedevent', event => { - const userDefinedEvent: string = event.payload.userDefinedEvent - handler(userDefinedEvent, event, settings) + manager.addEventListener('ecommerce', event => { + if (typeof event.name === 'string') { + const ev = mapEcommerceEvent(event.name) + if (ev) { + handler(event, settings, ev) + } + } }) }