From 51b188c71a454b0f3b75982decf9d51729fe6c3c Mon Sep 17 00:00:00 2001 From: Vitor Date: Mon, 20 Jul 2026 19:16:41 -0300 Subject: [PATCH] fix(analytics): Correct Awin conversion tracking data and add fallback pixel - send-to-awin.ts: use the store's real currency instead of a hardcoded BRL fallback, fix a bug where product category was never actually sent (wrong field name), validate traffic channel against Awin's accepted values, and sanitize basket fields against forbidden pipe characters - Add a client-side fallback conversion pixel, redundant to the existing server-to-server call, per Awin's tracking policy (packages/storefront) - Expose the Awin advertiser ID to the storefront client for the pixel Co-Authored-By: Claude Sonnet 5 --- .../ssr/src/lib/analytics/send-to-awin.ts | 26 +++++++++++++------ packages/storefront/client.d.ts | 1 + .../storefront/src/lib/layouts/BaseHead.astro | 1 + .../storefront/src/lib/scripts/vbeta-app.ts | 19 +++++++++++++- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/ssr/src/lib/analytics/send-to-awin.ts b/packages/ssr/src/lib/analytics/send-to-awin.ts index e44690f53..fceb5e964 100644 --- a/packages/ssr/src/lib/analytics/send-to-awin.ts +++ b/packages/ssr/src/lib/analytics/send-to-awin.ts @@ -1,8 +1,8 @@ import type { AnalyticsEvent } from './send-analytics-events'; -import { logger } from '@cloudcommerce/firebase/lib/config'; +import config, { logger } from '@cloudcommerce/firebase/lib/config'; import axios from 'axios'; -// https://developer.awin.com/apidocs/conversion-api +// https://help.awin.com/apidocs/conversion-api const { AWIN_ADVERTISER_ID, AWIN_API_KEY, @@ -18,6 +18,16 @@ const awinAxios = AWIN_ADVERTISER_ID && AWIN_API_KEY }) : null; +// https://help.awin.com/apidocs/conversion-api - accepted "channel" values +const validChannels = new Set([ + 'aw', 'ppcgeneric', 'ppcbrand', 'display', 'social', 'Other', 'Organic', 'direct', +]); + +// Awin forbids the pipe character in id/name/sku/category basket fields +const stripPipes = (value: unknown) => ( + typeof value === 'string' ? value.replace(/\|/g, '') : value +); + const sendToAwin = async ({ events, awc, @@ -36,23 +46,23 @@ const sendToAwin = async ({ const voucher = params.coupon; const awinOrder: Record = { orderReference: params.transaction_id, - channel, + channel: validChannels.has(channel) ? channel : 'aw', awc, voucher, amount: params.value, - currency: params.currency || 'BRL', + currency: params.currency || config.get().currency, commissionGroups: [{ code: 'DEFAULT', amount: params.value, }], basket: params.items?.map((item: Record) => ({ - id: item.object_id || item.item_id, - name: item.item_name, + id: stripPipes(item.object_id || item.item_id), + name: stripPipes(item.item_name), price: item.price, quantity: item.quantity || 1, commissionGroupCode: 'DEFAULT', - sku: item.item_id, - category: item.category || item.item_brand || item.item_id, + sku: stripPipes(item.item_id), + category: stripPipes(item.item_category || item.item_brand || item.item_id), })), }; awinOrders.push(awinOrder); diff --git a/packages/storefront/client.d.ts b/packages/storefront/client.d.ts index 90b32a74e..f9285b264 100644 --- a/packages/storefront/client.d.ts +++ b/packages/storefront/client.d.ts @@ -34,6 +34,7 @@ interface Window { GTAG_TAG_ID?: string; GA_TRACKING_ID?: string; GOOGLE_ADS_ID?: string; + AWIN_ADVERTISER_ID?: string; CMS_CUSTOM_CONFIG?: Record; CMS_SSO_URL?: string | null; CMS_REPO_BASE_DIR?: string; diff --git a/packages/storefront/src/lib/layouts/BaseHead.astro b/packages/storefront/src/lib/layouts/BaseHead.astro index 4e504a9b7..e404500c9 100644 --- a/packages/storefront/src/lib/layouts/BaseHead.astro +++ b/packages/storefront/src/lib/layouts/BaseHead.astro @@ -94,6 +94,7 @@ window.ECOM_COUNTRY_CODE = '${countryCode}'; window.GIT_BRANCH = '${import.meta.env.GIT_BRANCH || ''}'; window.GIT_REPO = '${import.meta.env.GIT_REPO || import.meta.env.GITHUB_REPO || ''}'; window.GCLOUD_PROJECT = '${import.meta.env.GCLOUD_PROJECT || ''}'; +window.AWIN_ADVERTISER_ID = '${import.meta.env.AWIN_ADVERTISER_ID || ''}'; window.$storefront = ${JSON.stringify({ settings, data: {} })};`; if (apiContext.error) { const { message, statusCode } = apiContext.error; diff --git a/packages/storefront/src/lib/scripts/vbeta-app.ts b/packages/storefront/src/lib/scripts/vbeta-app.ts index 1e4c38857..7b7f68ea5 100644 --- a/packages/storefront/src/lib/scripts/vbeta-app.ts +++ b/packages/storefront/src/lib/scripts/vbeta-app.ts @@ -26,9 +26,25 @@ import { isAuthReady, } from '@@sf/state/customer-session'; import { shoppingCart } from '@@sf/state/shopping-cart'; -import { emitGtagEvent, getGtagItem } from '@@sf/state/use-analytics'; +import { emitGtagEvent, getGtagItem, trackingIds } from '@@sf/state/use-analytics'; import utm from '@@sf/scripts/session-utm'; +// https://help.awin.com/apidocs - fallback pixel, redundant to the server-to-server +// Conversion API call already made from send-to-awin.ts +const emitAwinFallbackPixel = (orderId: string, amount: number, coupon?: string) => { + if (!window.AWIN_ADVERTISER_ID || !trackingIds.awc) return; + const src = 'https://www.awin1.com/sread.img' + + `?tt=ns&tv=2&merchant=${encodeURIComponent(window.AWIN_ADVERTISER_ID)}` + + `&amount=${amount}&cr=${encodeURIComponent(window.ECOM_CURRENCY)}` + + `&ref=${encodeURIComponent(orderId)}` + + `&parts=${encodeURIComponent(`DEFAULT:${amount}`)}` + + `&vc=${encodeURIComponent(coupon || '')}` + + `&ch=${encodeURIComponent(trackingIds.awin_channel || 'aw')}` + + '&testmode=0'; + const img = new Image(0, 0); + img.src = src; +}; + const watchAppRoutes = () => { const router = (window as any).storefrontApp?.router; if (router) { @@ -140,6 +156,7 @@ const watchAppRoutes = () => { params.shipping_delivery_days = days; } emitGtagEvent('purchase', params, paramsToHash); + emitAwinFallbackPixel(orderId, params.value as number, params.coupon); localStorage.setItem('gtag.orderIdSent', orderId); } isPurchaseSent = true;