Last active
November 3, 2025 11:18
-
-
Save aanushh/d1fd49a58b90eb2fde73b0498761602d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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.