Skip to content

Instantly share code, notes, and snippets.

@simone-coelho
Last active October 31, 2024 18:53
Show Gist options
  • Select an option

  • Save simone-coelho/1e8fb026a4ca4f1f0d3ed7c37a4d4ec4 to your computer and use it in GitHub Desktop.

Select an option

Save simone-coelho/1e8fb026a4ca4f1f0d3ed7c37a4d4ec4 to your computer and use it in GitHub Desktop.
const axios = require('axios');
/**
* Maps mParticle event data to Optimizely's Full Stack event format
* @param {MParticleEvent} mpEvent - The mParticle event object
* @param {Array} eventMap - List of events with their corresponding IDs
* @returns {OptimizelyEvent} Formatted Optimizely event payload
*/
function createOptimizelyPayload(mpEvent, eventMap) {
if (!mpEvent?.data?.custom_event_type) {
throw new Error('Invalid mParticle event data: custom_event_type is required');
}
// Retrieve visitor ID (fallback to UUID if no customer_id or email is available)
const visitorId = mpEvent.user_identities?.customer_id ||
mpEvent.user_identities?.email ||
generateUUID();
// Find corresponding Full Stack event ID for the custom event type
const entityId = findEntityId(mpEvent.data.custom_event_type, eventMap);
if (!entityId) {
throw new Error(`Event ${mpEvent.data.custom_event_type} does not exist in Full Stack event mapping`);
}
// Build visitor attributes, including bot filtering
const attributes = buildVisitorAttributes(mpEvent.data.custom_attributes || {});
return {
account_id: process.env.OPTIMIZELY_ACCOUNT_ID,
project_id: process.env.OPTIMIZELY_PROJECT_ID,
revision: "100", // revision of the datafile, can be made dynamic
visitors: [{
visitor_id: visitorId,
attributes,
snapshots: [{
decisions: [],
events: [{
entity_id: entityId,
key: mpEvent.data.custom_event_type.replace(/ /g, '_'),
timestamp: Date.now(),
uuid: generateUUID(),
revenue: mpEvent.data.custom_attributes?.revenue || 0,
value: mpEvent.data.custom_attributes?.value || 0
}]
}]
}],
anonymize_ip: true,
client_name: "Optimizely/fswebintegration",
client_version: "1.0.0",
enrich_decisions: true
};
}
/**
* Finds the entity ID for an event in the Full Stack datafile mapping
* @param {string} eventName - The event name from mParticle
* @param {Array} eventMap - Event mapping list from Full Stack datafile
* @returns {string|null} The entity ID of the event or null if not found
*/
function findEntityId(eventName, eventMap) {
const event = eventMap.find(e => e.key === eventName);
return event ? event.id : null;
}
/**
* Builds visitor attributes array, adding bot filtering if not present
* @param {Object} customAttributes - Custom attributes from mParticle event
* @returns {Array<Object>} Array of attribute objects for the visitor
*/
function buildVisitorAttributes(customAttributes) {
const attributes = Object.entries(customAttributes).map(([key, value]) => ({
entity_id: null,
key,
type: "custom",
value
}));
// Add bot filtering attribute
attributes.push({
entity_id: "$opt_bot_filtering",
key: "$opt_bot_filtering",
type: "custom",
value: false
});
return attributes;
}
/**
* Generates a UUID v4
* @returns {string} UUID string
*/
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* Sends event data to Optimizely's LogX endpoint
* @param {OptimizelyEvent} payload - The Optimizely event payload
* @returns {Promise<Object>} Response from Optimizely API
*/
async function sendToOptimizely(payload) {
try {
const response = await axios.post('https://logx.optimizely.com/v1/events', payload, {
headers: {
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
console.error('Error sending event to Optimizely:', error.message);
throw error;
}
}
/**
* Main Lambda handler function
* @param {Object} event - AWS Lambda event object containing mParticle data
* @param {Object} context - AWS Lambda context
* @returns {Promise<Object>} Lambda response
*/
exports.handler = async (event, context) => {
try {
// Validate required environment variables
if (!process.env.OPTIMIZELY_PROJECT_ID || !process.env.OPTIMIZELY_ACCOUNT_ID) {
throw new Error('OPTIMIZELY_PROJECT_ID and OPTIMIZELY_ACCOUNT_ID environment variables are required');
}
// Parse and validate mParticle event data
const mpEvent = JSON.parse(event.body);
if (mpEvent.event_type !== 'custom_event') {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Event type not tracked' })
};
}
// Fetch the Full Stack event mapping datafile if needed
const eventMap = await fetchEventMapping();
// Create Optimizely payload
let optimizelyPayload;
try {
optimizelyPayload = createOptimizelyPayload(mpEvent, eventMap);
} catch (err) {
console.error('Error creating Optimizely payload:', err.message);
throw new Error('Failed to create Optimizely payload');
}
// Send event to Optimizely
await sendToOptimizely(optimizelyPayload);
return {
statusCode: 200,
body: JSON.stringify({
message: 'Event successfully forwarded to Optimizely',
eventName: optimizelyPayload.visitors[0].snapshots[0].events[0].key
})
};
} catch (error) {
console.error('Error processing event:', error.message);
return {
statusCode: 500,
body: JSON.stringify({
message: 'Error processing event',
error: error.message
})
};
}
};
/**
* This is an example of how you can support the event mapping between Optimizely and Marticle
* This would require making an additional HTTP request. A better option is to provide a cached
* dictionary, or embed it into the rule's code itself to avoide adding latency from a request.
*
* Fetches the Optimizely datafile and parses event mappings.
* @returns {Array} Array of events with id and key properties
*/
async function fetchEventMapping() {
const lastFetch = localStorage.getItem('OptlyDatafileFetch') || 0;
const needsUpdate = Date.now() - lastFetch > 86400 * 1000; // Update every 24 hours
if (needsUpdate) {
const response = await axios.get(`https://cdn.optimizely.com/datafiles/${process.env.OPTIMIZELY_ENV_KEY}.json`);
const datafile = response.data;
localStorage.setItem('OptlyDatafile', JSON.stringify(datafile));
localStorage.setItem('OptlyDatafileFetch', Date.now());
}
const datafile = JSON.parse(localStorage.getItem('OptlyDatafile') || '{}');
return datafile.events?.map(event => ({ id: event.id, key: event.key })) || [];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment