Skip to content

Instantly share code, notes, and snippets.

@aanushh
Last active November 3, 2025 11:18
Show Gist options
  • Select an option

  • Save aanushh/d1fd49a58b90eb2fde73b0498761602d to your computer and use it in GitHub Desktop.

Select an option

Save aanushh/d1fd49a58b90eb2fde73b0498761602d to your computer and use it in GitHub Desktop.
/**
* Zalora Segment Event to POND API Schema Transformer
*
* This function transforms a Segment event object into the specified POND API schema
* based on the mapping tables provided in the document.
*
* @param {object} event The incoming Segment event object.
* @returns {object|null} The transformed POND payload object, or null if the event type is unhandled.
*/
function transformEvent(event) {
const POND_SCHEMA_VERSION = 3;
const SUPPORTED_EVENTS = ['Product Viewed', 'Product Added', 'Product Added to Wishlist']
if(!SUPPORTED_EVENTS.includes(event.event)) {
return null;
}
// Helper function to convert ISO 8601 string to Unix timestamp in seconds
const toUnixSeconds = isoString => {
if (!isoString) return null;
// The timestamp in the sample is in milliseconds, so we divide by 1000
return Math.floor(new Date(isoString).getTime() / 1000);
};
// Helper function for price discount calculation
const calculateDiscount = (originalPrice, currentPrice) => {
if (typeof originalPrice !== 'number' || typeof currentPrice !== 'number') {
return 0;
}
// max(price_original - price, 0), round to 5 decimal places
const discount = Math.max(originalPrice - currentPrice, 0);
return parseFloat(discount.toFixed(5));
};
// Get core properties for simplicity
const properties = event.properties || {};
const context = event.context || {};
const device = context.device || {};
const os = context.os || {};
const traits = context.traits || {};
const app = context.app || {};
// Base POND structure with common fields
const pondPayload = {
schema_version: POND_SCHEMA_VERSION,
event: {
event_id: event.messageId, // Keep the original value [cite: 560, 562, 564]
type: event.event ? event.event.toLowerCase().replace(/ /g, '') : null, // Convert event name to lowercase, remove spaces [cite: 560, 562, 564]
created_at: toUnixSeconds(event.timestamp), // Convert to unix timestamp in seconds [cite: 560, 562, 564]
sent_at: toUnixSeconds(event.sentAt), // Convert to unix timestamp in seconds [cite: 560, 562, 564]
local_timestamp: toUnixSeconds(event.originalTimestamp), // Convert to unix timestamp in seconds [cite: 560, 562, 564]
details: {
list_name: properties.list_id || 'UNKNOWN', // Keep the original value [cite: 560, 562, 564]
list_id: properties.list_id || 'UNKNOWN', // Keep the original value [cite: 560, 562, 564]
query: properties.list_id || 'UNKNOWN', // Keep the original value [cite: 560, 562, 564]
query_type: properties.source_catalog || 'UNKNOWN', // Keep the original value [cite: 560, 562, 564]
items: [
{
details: {
group_id: properties.sku, // Product SKU config [cite: 560, 562, 564]
id: properties.sku_simple || properties.product_id, // Product SKU simple [cite: 560, 562, 564]
stock_count:
properties.config_sku_stock_count ||
properties.simple_sku_stock_count ||
1, // Default to 0 if both are missing [cite: 560, 562, 564]
brand: properties.brand, // Keep the original value [cite: 560, 562, 564]
category: properties.category, // Keep the original value [cite: 560, 562, 564]
sizes: ['UNKNOWN'], // SKIP/MISSING in Segment event [cite: 560, 562, 564]
size_standard: 'UNKNOWN', // SKIP/MISSING in Segment event [cite: 560, 562, 564]
size_value: 'UNKNOWN' // SKIP/MISSING in Segment event [cite: 560, 562, 564]
// stock_count, sizes, size_standard, size_value are event-dependent
},
price: {
current: properties.price, // Keep the original value [cite: 560, 562, 564]
previous: properties.price_original, // Keep the original value [cite: 560, 562, 564]
discount: calculateDiscount(
properties.price_original,
properties.price
), // Calculate max(price_original - price, 0) [cite: 560, 562, 564]
current_in_usd: 0 // MISSING/Defaulted in mapping, setting to 0 as in proposed payload [cite: 451]
},
provider: 'zalora-segmentio-pond-insert-function' // Default provider as per proposed payload [cite: 448]
}
]
},
device: {
platform:
device.type.includes('ios') || device.type.includes('android')
? 'mobile'
: 'desktop', // First non-empty value from context.device.type, context.userAgentData.platform [cite: 560, 562, 564]
client: context.userAgent, // Keep the original value [cite: 560, 562, 564]
model: device.model, // Keep the original value [cite: 560, 562, 564]
os: device.type || null, // First non-empty value from context.device.type, context.userAgentData.platform [cite: 560, 562, 564]
os_version: os.version || device.os_version || null // Varies by event type [cite: 560, 562, 564]
// origin and platform are complex and event-dependent
},
identifiers: {
device_id: {
type: 'device_id', // Default value to "device_id" [cite: 560, 562, 564]
value: device.deviceId || device.id // First non-empty value from context.device.deviceId, context.device.id [cite: 560, 562, 564]
},
anonymous_id: {
type: 'anonymous_id', // Default value to "anonymous_id" [cite: 560, 562, 564]
value: event.anonymousId // First non-empty value from context.internal_anonymous_id, anonymousId [cite: 560, 562, 564]
},
user_id: {
type: 'user_id', // Default value to "user_id" [cite: 560, 562, 564]
value: event.userId || traits.userId // First non-empty value from userId, context.traits.user_id [cite: 560, 562, 564]
}
},
source: {
url_referrer: traits.referrer_url || traits.page_url || '', // For web origin. context.traits.referrer_url [cite: 560, 562, 564]
url: traits.referrer_url || traits.page_url || '' // For web origin. properties.page_url, context.traits.page_url [cite: 560, 562, 564]
// url is MISSING/not required for app events
},
user: {
gender: traits.email_gender
// age, location, register_time are MISSING/SKIP
},
app: {
name: app.name, // Keep the original value [cite: 560, 562, 564]
version: app.version // Keep the original value [cite: 560, 562, 564]
}
}
};
// --- Event-Specific Logic ---
switch (event.event) {
case 'Product Viewed': {
// Mapping Logic for Product Viewed [cite: 560]
pondPayload.event.screen_name = 'PRODUCT_DETAILS'; // Hardcoded as per proposed payload [cite: 433]
// 1. Stock Count Mapping
(pondPayload.event.details.items[0].details.stock_count =
properties.config_sku_stock_count ||
properties.simple_sku_stock_count ||
1),
// 2. OS Version
(pondPayload.event.device.os_version = os.version); // context.device.os_version [cite: 560] - using context.os.version as it's present in sample
// 3. Platform & Origin logic for app
if (app && Object.keys(app).length > 0) {
// If context.app is not empty
pondPayload.event.device.origin = 'app'; // event.device.platform [cite: 560]
}
// 4. Collect unmapped Segment properties into extra_info
// NOTE: For brevity and based on the instruction "All other unmapped fields can be put into this field."
// we will simply include the entire 'properties' object here for demonstration,
// as manually listing all unmapped fields is impractical.
pondPayload.event.extra_info = properties;
// Overwrite event type to 'productview' (as per proposed payload)
pondPayload.event.type = 'productview'; // Use 'productview' from the proposed payload sample [cite: 433]
break;
}
case 'Product Added': {
// Mapping Logic for AddToCart [cite: 562]
pondPayload.event.screen_name = properties.source; // Hardcoded as per proposed payload [cite: 433]
// 1. Stock Count Mapping
pondPayload.event.details.items[0].details.stock_count =
properties.simple_sku_stock_count ||
properties.config_sku_stock_count ||
1;
// 2. Size Value
pondPayload.event.details.items[0].details.size_value =
properties.size || 'One Size'; // properties.size [cite: 562]
// 3. OS Version
pondPayload.event.device.os_version = os.version || device.os_version; // First non-empty value from context.device.os_version, context.os.version [cite: 562]
// 4. Platform & Origin logic for app
if (app && Object.keys(app).length > 0) {
// If context.app is not empty
pondPayload.event.device.origin = 'app'; // event.device.platform [cite: 560]
}
// 5. Collect unmapped Segment properties into extra_info
pondPayload.event.extra_info = properties;
pondPayload.event.type = 'addtocart'; // Use 'productview' from the proposed payload sample [cite: 433]
break;
}
case 'Product Added to Wishlist': {
// Mapping Logic for AddToWishList [cite: 564]
pondPayload.event.screen_name = properties.source; // Hardcoded as per proposed payload [cite: 433]
// 1. Stock Count Mapping (First non-empty value from properties.simple_sku_stock_count, properties.config_sku_stock_count)
pondPayload.event.details.items[0].details.stock_count =
properties.simple_sku_stock_count ||
properties.config_sku_stock_count ||
1; // Keep the original value [cite: 564]
// 2. Size Value
pondPayload.event.details.items[0].details.size_value =
properties.size || 'UNKNOWN_WISHLIST'; // properties.size [cite: 564]
// 3. OS Version
pondPayload.event.device.os_version = os.version || device.os_version; // First non-empty value from context.device.os_version, context.os.version [cite: 564]
// 4. Platform & Origin logic for app
if (app && Object.keys(app).length > 0) {
// If context.app is not empty
pondPayload.event.device.origin = 'app'; // event.device.platform [cite: 560]
}
// 5. Collect unmapped Segment properties into extra_info
pondPayload.event.extra_info = properties;
pondPayload.event.type = 'addtowishlist'; // Use 'productview' from the proposed payload sample [cite: 433]
break;
}
default:
// Handle unmapped event types
return null;
}
// Clean up empty 'extra_info' if no properties were copied
if (Object.keys(pondPayload.event.extra_info).length === 0) {
delete pondPayload.event.extra_info;
}
return pondPayload;
}
/**
* Handle track event
* @param {SegmentTrackEvent} event
* @param {FunctionSettings} settings
*/
async function onTrack(event, settings) {
// Learn more at https://segment.com/docs/connections/spec/track/
let response;
let pondReadyEvent = transformEvent(event);
if (pondReadyEvent == null) {
return event;
}
// console.log(`Sending to POND : ${JSON.stringify(pondReadyEvent)}`);
const pondApiKey = {
ID: 'o1DkMSe2QBdwWr7MwbKaXahJX2hMswv',
SG: 'HGCR17ER0xdwbpKlgAHsQf400MpjnTR',
MY: 'oZZ0vgj8xdjNnkK7AfXu0qH1WHUynBC',
PH: 'UylVL1r1VGgSfaXAehIB9VJhsUDEIgJ',
HK: 'YlPRqPhEXaX3VyP4MM0YO6lEhWgwtyU'
};
let endpoint =
'https://pond.asse1.datajet.io/v2/log?key=YLGEaBdsuneUObwCMWlKXYbM4m0XC6p'; // replace with your endpoint
const shopCountry = event.context.traits.shop_country;
if (shopCountry) {
const countryCode = shopCountry.toUpperCase();
endpoint = `https://pond.asse1.datajet.io/v2/log?key=${pondApiKey[countryCode] || pondApiKey['SG']}`;
}
try {
response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(pondReadyEvent)
});
console.log(`POND Response : ${JSON.stringify(await response.text())}`);
} catch (error) {
// Retry on connection error
return event;
}
if (response.status >= 500 || response.status === 429) {
// Retry on 5xx (server errors) and 429s (rate limits)
return event;
}
return event;
}
/**
* Handle identify event
* @param {SegmentIdentifyEvent} event
* @param {FunctionSettings} settings
*/
async function onIdentify(event, settings) {
// Learn more at https://segment.com/docs/connections/spec/identify/
return event;
}
/**
* Handle group event
* @param {SegmentGroupEvent} event
* @param {FunctionSettings} settings
*/
async function onGroup(event, settings) {
// Learn more at https://segment.com/docs/connections/spec/group/
return event;
}
/**
* Handle page event
* @param {SegmentPageEvent} event
* @param {FunctionSettings} settings
*/
async function onPage(event, settings) {
// Learn more at https://segment.com/docs/connections/spec/page/
return event;
}
/**
* Handle screen event
* @param {SegmentScreenEvent} event
* @param {FunctionSettings} settings
*/
async function onScreen(event, settings) {
// Learn more at https://segment.com/docs/connections/spec/screen/
return event;
}
/**
* Handle alias event
* @param {SegmentAliasEvent} event
* @param {FunctionSettings} settings
*/
async function onAlias(event, settings) {
// Learn more at https://segment.com/docs/connections/spec/alias/
return event;
}
/**
* Handle delete event
* @param {SegmentDeleteEvent} event
* @param {FunctionSettings} settings
*/
async function onDelete(event, settings) {
// Learn more at https://segment.com/docs/partners/spec/#delete
return event;
}

Comments are disabled for this gist.