mate & me

mate & me

mate & me

mate & me

mate & me

mate & me

mate & me

mate & me

The first his & her prenatal gummies

Est.
Project
mate & me
Industry
Wellness
Roles

Custom Shopify CSS/JS/Liquid

3rd-Party Integration

Website

The Process

'use strict';

/**
* Module dependencies.
*/

var integration = require('@segment/analytics.js-integration');
var each = require('@ndhoule/each');
var reject = require('reject');
var camel = require('to-camel-case');
var is = require('is');
var dateformat = require('dateformat');
var Track = require('segmentio-facade').Track;
var sha256 = require('js-sha256');

/**
* Expose `Facebook Pixel`.
*/

var FacebookPixel = (module.exports = integration('Facebook Pixel')
 .global('fbq')
 .option('pixelId', '')
 .option('agent', 'seg')
 .option('valueIdentifier', 'value')
 .option('initWithExistingTraits', false)
 .option('traverse', false)
 .option('automaticConfiguration', true)
 .option('whitelistPiiProperties', [])
 .option('blacklistPiiProperties', [])
 .option('standardEventsCustomProperties', [])
 .option('keyForExternalId', '')
 .option('userIdAsExternalId', false)
 .option('limitedDataUse', true)
 .mapping('standardEvents')
 .mapping('legacyEvents')
 .mapping('contentTypes')
 .tag('<script src="//connect.facebook.net/en_US/fbevents.js">'));

/**
* FB requires these date fields be formatted in a specific way.
* The specifications are non iso8601 compliant.
* https://developers.facebook.com/docs/marketing-api/dynamic-ads-for-travel/audience
* Therefore, we check if the property is one of these reserved fields.
* If so, we check if we have converted it to an iso date object already.
* If we have, we convert it again into Facebook's spec.
* If we have not, the user has likely passed in a date string that already
* adheres to FB's docs so we can just pass it through as is.
*/
var dateFields = [
 'checkinDate',
 'checkoutDate',
 'departingArrivalDate',
 'departingDepartureDate',
 'returningArrivalDate',
 'returningDepartureDate',
 'travelEnd',
 'travelStart'
];




/**
* FB does not allow sending PII data with events. They provide a list of what they consider PII here:
* https://developers.facebook.com/docs/facebook-pixel/pixel-with-ads/conversion-tracking
* We need to check each property key to see if it matches what FB considers to be a PII property and strip it from the payload.
* User's can override this by manually whitelisting keys they are ok with sending through in their integration settings.
*/
var defaultPiiProperties = [
 'email',
 'firstName',
 'lastName',
 'gender',
 'city',
 'country',
 'phone',
 'state',
 'zip',
 'birthday'
];

/**
* Initialize Facebook Pixel.
*
* @param {Facade} page
*/

FacebookPixel.prototype.initialize = function() {
 window._fbq = function() {
   if (window.fbq.callMethod) {
     window.fbq.callMethod.apply(window.fbq, arguments);
   } else {
     window.fbq.queue.push(arguments);
   }
 };

 window.fbq = window.fbq || window._fbq;
 window.fbq.push = window.fbq;
 window.fbq.loaded = true;
 window.fbq.disablePushState = true; // disables automatic pageview tracking
 window.fbq.allowDuplicatePageViews = true; // enables fb
 window.fbq.agent = this.options.agent;
 window.fbq.version = '2.0';
 window.fbq.queue = [];
 this.load(this.ready);
 if (!this.options.automaticConfiguration) {
   window.fbq('set', 'autoConfig', false, this.options.pixelId);
 }
 if (this.options.limitedDataUse) {
   this.validateAndSetDataProcessing(
     this.options.dataProcessingOptions || [['LDU'], 0, 0]
   );
 } else {
   // explicitly not enable Limited Data Use (LDU) mode
   window.fbq('dataProcessingOptions', []);
 }
 if (this.options.initWithExistingTraits) {
   var traits = this.formatTraits(this.analytics);
   window.fbq('init', this.options.pixelId, traits);
 } else {
   window.fbq('init', this.options.pixelId);
 }
};

/**
* Has the Facebook Pixel library been loaded yet?
*
* @return {Boolean}
*/

FacebookPixel.prototype.loaded = function() {
 return !!(window.fbq && window.fbq.callMethod);
};

/**
* Trigger a page view.
*
* @param {Facade} identify
*/

FacebookPixel.prototype.page = function(track) {
 window.fbq('track', 'PageView', {}, { eventID: track.proxy('messageId') });
};

/**
* Track an event.
*
* @param {Facade} track
*/

FacebookPixel.prototype.track = function(track) {
 var self = this;
 var event = track.event();
 var revenue = formatRevenue(track.revenue());
 var payload = this.buildPayload(track);

 // Revenue
 if (track.properties().hasOwnProperty('revenue')) {
   payload.value = formatRevenue(track.revenue());
   // To keep compatible with the old implementation
   // that never added revenue to the payload
   delete payload.revenue;
 }

 var standard = this.standardEvents(event);
 var legacy = this.legacyEvents(event);

 // non-mapped events get sent as "custom events" with full
 // tranformed payload
 if (![].concat(standard, legacy).length) {
   window.fbq('trackSingleCustom', this.options.pixelId, event, payload, {
     eventID: track.proxy('messageId')
   });
   return;
 }

 // standard conversion events, mapped to one of 9 standard events
 // "Purchase" requires a currency parameter;
 // send full transformed payload
 each(function(event) {
   if (event === 'Purchase') payload.currency = track.currency(); // defaults to 'USD'
   window.fbq('trackSingle', self.options.pixelId, event, payload, {
     eventID: track.proxy('messageId')
   });
 }, standard);

 // legacy conversion events — mapped to specific "pixelId"s
 // send only currency and value
 each(function(event) {
   window.fbq(
     'trackSingle',
     self.options.pixelId,
     event,
     {
       currency: track.currency(),
       value: revenue
     },
     { eventID: track.proxy('messageId') }
   );
 }, legacy);
};

/**
* Product List Viewed.
*
* @api private
* @param {Track} track category
*/

FacebookPixel.prototype.productListViewed = function(track) {
 var self = this;
 var contentType;
 var contentIds = [];
 var contents = [];
 var products = track.products();
 var customProperties = this.buildPayload(track, true);

 // First, check to see if a products array with productIds has been defined.
 if (Array.isArray(products)) {
   products.forEach(function(product) {
     var track = new Track({ properties: product });
     var productId =
       track.proxy('properties.product_id') || track.productId() || track.id();

     if (productId) {
       contentIds.push(productId);
       contents.push({
         id: productId,
         quantity: track.quantity()
       });
     }
   });
 }

 // If no products have been defined, fallback on legacy behavior.
 // Facebook documents the content_type parameter decision here: https://developers.facebook.com/docs/facebook-pixel/api-reference
 if (contentIds.length) {
   contentType = ['product'];
 } else {
   contentIds.push(track.category() || '');
   contents.push({
     id: track.category() || '',
     quantity: 1
   });
   contentType = ['product_group'];
 }

 window.fbq(
   'trackSingle',
   this.options.pixelId,
   'ViewContent',
   merge(
     {
       content_ids: contentIds,
       content_type: this.getContentType(track, contentType),
       contents: contents
     },
     customProperties
   ),
   { eventID: track.proxy('messageId') }
 );

 // fall through for mapped legacy conversions
 each(function(event) {
   window.fbq(
     'trackSingle',
     self.options.pixelId,
     event,
     {
       currency: track.currency(),
       value: formatRevenue(track.revenue())
     },
     { eventID: track.proxy('messageId') }
   );
 }, this.legacyEvents(track.event()));
};

/**
* Product viewed.
*
* @api private
* @param {Track} track
*/

FacebookPixel.prototype.productViewed = function(track) {
 var self = this;
 var useValue = this.options.valueIdentifier === 'value';
 var customProperties = this.buildPayload(track, true);

 window.fbq(
   'trackSingle',
   this.options.pixelId,
   'ViewContent',
   merge(
     {
       content_ids: [track.productId() || track.id() || track.sku() || ''],
       content_type: this.getContentType(track, ['product']),
       content_name: track.name() || '',
       content_category: track.category() || '',
       currency: track.currency(),
       value: useValue
         ? formatRevenue(track.value())
         : formatRevenue(track.price()),
       contents: [
         {
           id: track.productId() || track.id() || track.sku() || '',
           quantity: track.quantity(),
           item_price: track.price()
         }
       ]
     },
     customProperties
   ),
   { eventID: track.proxy('messageId') }
 );

 // fall through for mapped legacy conversions
 each(function(event) {
   window.fbq(
     'trackSingle',
     self.options.pixelId,
     event,
     {
       currency: track.currency(),
       value: useValue
         ? formatRevenue(track.value())
         : formatRevenue(track.price())
     },
     { eventID: track.proxy('messageId') }
   );
 }, this.legacyEvents(track.event()));
};

/**
* Product added.
*
* @api private
* @param {Track} track
*/

FacebookPixel.prototype.productAdded = function(track) {
 var self = this;
 var useValue = this.options.valueIdentifier === 'value';
 var customProperties = this.buildPayload(track, true);

 window.fbq(
   'trackSingle',
   this.options.pixelId,
   'AddToCart',
   merge(
     {
       content_ids: [track.productId() || track.id() || track.sku() || ''],
       content_type: this.getContentType(track, ['product']),
       content_name: track.name() || '',
       content_category: track.category() || '',
       currency: track.currency(),
       value: useValue
         ? formatRevenue(track.value())
         : formatRevenue(track.price()),
       contents: [
         {
           id: track.productId() || track.id() || track.sku() || '',
           quantity: track.quantity(),
           item_price: track.price()
         }
       ]
     },
     customProperties
   ),
   { eventID: track.proxy('messageId') }
 );

 // fall through for mapped legacy conversions
 each(function(event) {
   window.fbq(
     'trackSingle',
     self.options.pixelId,
     event,
     {
       currency: track.currency(),
       value: useValue
         ? formatRevenue(track.value())
         : formatRevenue(track.price())
     },
     { eventID: track.proxy('messageId') }
   );
 }, this.legacyEvents(track.event()));
};

/**
* Order Completed.
*
* @api private
* @param {Track} track
*/

FacebookPixel.prototype.orderCompleted = function(track) {
 var self = this;
 var products = track.products();
 var customProperties = this.buildPayload(track, true);

 var revenue = formatRevenue(track.revenue());

 // Order completed doesn't have a top-level category spec'd.
 // Let's default to the category of the first product. - @gabriel

 var contentType = this.getContentType(track, ['product']);
 var contentIds = [];
 var contents = [];

 for (var i = 0; i < products.length; i++) {
   var trackItem = new Track({ properties: products[i] });
   var pId = trackItem.productId() || trackItem.id() || trackItem.sku();
   contentIds.push(pId);
   var content = {
     id: pId,
     quantity: trackItem.quantity()
   };
   if (trackItem.price()) {
     content.item_price = trackItem.price();
   }
   contents.push(content);
 }

 window.fbq(
   'trackSingle',
   this.options.pixelId,
   'Purchase',
   merge(
     {
       content_ids: contentIds,
       content_type: contentType,
       currency: track.currency(),
       value: revenue,
       contents: contents,
       num_items: contentIds.length
     },
     customProperties
   ),
   { eventID: track.proxy('messageId') }
 );

 // fall through for mapped legacy conversions
 each(function(event) {
   window.fbq(
     'trackSingle',
     self.options.pixelId,
     event,
     {
       currency: track.currency(),
       value: formatRevenue(track.revenue())
     },
     { eventID: track.proxy('messageId') }
   );
 }, this.legacyEvents(track.event()));
};

FacebookPixel.prototype.productsSearched = function(track) {
 var self = this;
 var customProperties = this.buildPayload(track, true);

 window.fbq(
   'trackSingle',
   this.options.pixelId,
   'Search',
   merge(
     {
       search_string: track.proxy('properties.query')
     },
     customProperties
   ),
   { eventID: track.proxy('messageId') }
 );

 // fall through for mapped legacy conversions
 each(function(event) {
   window.fbq(
     'trackSingle',
     self.options.pixelId,
     event,
     {
       currency: track.currency(),
       value: formatRevenue(track.revenue())
     },
     { eventID: track.proxy('messageId') }
   );
 }, this.legacyEvents(track.event()));
};

FacebookPixel.prototype.checkoutStarted = function(track) {
 var self = this;
 var products = track.products();
 var contentIds = [];
 var contents = [];
 var contentCategory = track.category();
 var customProperties = this.buildPayload(track, true);

 for (var i = 0; i < products.length; i++) {
   var trackItem = new Track({ properties: products[i] });
   var pId = trackItem.productId() || trackItem.id() || trackItem.sku();
   contentIds.push(pId);
   var content = {
     id: pId,
     quantity: trackItem.quantity(),
     item_price: track.price()
   };
   if (trackItem.price()) {
     content.item_price = trackItem.price();
   }
   contents.push(content);
 }

 // If no top-level category was defined use that of the first product. @gabriel
 if (!contentCategory && products[0] && products[0].category) {
   contentCategory = products[0].category;
 }

 window.fbq(
   'trackSingle',
   this.options.pixelId,
   'InitiateCheckout',
   merge(
     {
       content_category: contentCategory,
       content_ids: contentIds,
       content_type: this.getContentType(track, ['product']),
       contents: contents,
       currency: track.currency(),
       num_items: contentIds.length,
       value: formatRevenue(track.revenue())
     },
     customProperties
   ),
   { eventID: track.proxy('messageId') }
 );

 // fall through for mapped legacy conversions
 each(function(event) {
   window.fbq(
     'trackSingle',
     self.options.pixelId,
     event,
     {
       currency: track.currency(),
       value: formatRevenue(track.revenue())
     },
     { eventID: track.proxy('messageId') }
   );
 }, this.legacyEvents(track.event()));
};

/**
* Returns an array of mapped content types for the category,
* the provided value as an integration option or the default provided value.
*
* @param {Facade.Track} track Track payload
* @param {Array} defaultValue Default array value returned if the previous parameters are not defined.
*
* @return Content Type array as defined in:
* - https://developers.facebook.com/docs/facebook-pixel/reference/#object-properties
* - https://developers.facebook.com/docs/marketing-api/dynamic-ads-for-real-estate/audience
*/
FacebookPixel.prototype.getContentType = function(track, defaultValue) {
 // 1- Integration options takes preference over everything
 var options = track.options('Facebook Pixel');
 if (options && options.contentType) {
   return [options.contentType];
 }

 // 2- Defined by category and its mappings
 var category = track.category();
 if (!category) {
   // Get the first product's category
   var products = track.products();
   if (products && products.length) {
     category = products[0].category;
   }
 }

 if (category) {
   var mapped = this.contentTypes(category);
   if (mapped.length) {
     return mapped;
   }
 }

 // 3- The default value
 return defaultValue;
};

/**
* Get Revenue Formatted Correctly for FB.
*
* @api private
* @param {Track} track
*/

function formatRevenue(revenue) {
 return Number(revenue || 0).toFixed(2);
}

/**
* Get Traits Formatted Correctly for FB.
*
* https://developers.facebook.com/docs/facebook-pixel/pixel-with-ads/conversion-tracking#advanced_match
*
* @api private
*/
FacebookPixel.prototype.formatTraits = function formatTraits(analytics) {
 var traits = analytics && analytics.user().traits();
 if (!traits) return {};
 var firstName;
 var lastName;
 // Check for firstName property
 // else check for name
 if (traits.firstName || traits.first_name) {
   firstName = traits.firstName || traits.first_name;
   lastName = traits.lastName || traits.last_name;
 } else {
   var nameArray = (traits.name && traits.name.toLowerCase().split(' ')) || [];
   firstName = nameArray.shift();
   lastName = nameArray.pop();
 }
 var gender;
 if (traits.gender && is.string(traits.gender)) {
   gender = traits.gender.slice(0, 1).toLowerCase();
 }
 var birthday = traits.birthday && dateformat(traits.birthday, 'yyyymmdd');
 var address = traits.address || {};
 var city =
   address.city &&
   address.city
     .split(' ')
     .join('')
     .toLowerCase();
 var state = address.state && address.state.toLowerCase();
 var postalCode = address.postalCode || address.postal_code;
 var external_id; // eslint-disable-line
 if (this.options.keyForExternalId) {
   external_id = traits[this.options.keyForExternalId]; // eslint-disable-line
 }
 if (!external_id && this.options.userIdAsExternalId && analytics) { // eslint-disable-line
   external_id = analytics.user().id() || analytics.user().anonymousId(); // eslint-disable-line
 }
 return reject({
   em: traits.email,
   fn: firstName,
   ln: lastName,
   ph: traits.phone,
   ge: gender,
   db: birthday,
   ct: city,
   st: state,
   zp: postalCode,
   external_id: external_id  // eslint-disable-line
 });
};

/**
* Builds the FB Event payload. It checks for PII fields and custom properties. If the event is Standard Event,
* only properties defined in the setting are passed to the payload.
*
* @param {Facade.Track} track Track event.
* @param {boolean} isStandardEvent Defines if the track call is a standard event.
*
* @return Payload to send deriveded from the track properties.
*/
FacebookPixel.prototype.buildPayload = function(track, isStandardEvent) {
 var whitelistPiiProperties = this.options.whitelistPiiProperties || [];
 var blacklistPiiProperties = this.options.blacklistPiiProperties || [];
 var standardEventsCustomProperties =
   this.options.standardEventsCustomProperties || [];

 // Transforming the setting in a map for easier lookups.
 var customPiiProperties = {};
 for (var i = 0; i < blacklistPiiProperties.length; i++) {
   var configuration = blacklistPiiProperties[i];
   customPiiProperties[configuration.propertyName] =
     configuration.hashProperty;
 }

 var payload = {};
 var properties = track.properties();

 for (var property in properties) {
   if (!properties.hasOwnProperty(property)) {
     continue;
   }

   // Standard Events only contains custom properties defined in the configuration
   // If the property is not listed there, we just drop it.
   if (
     isStandardEvent &&
     standardEventsCustomProperties.indexOf(property) < 0
   ) {
     continue;
   }

   var value = properties[property];

   // Dates
   if (dateFields.indexOf(camel(property)) >= 0) {
     if (is.date(value)) {
       payload[property] = value.toISOString().split('T')[0];
       continue;
     }
   }

   // Custom PII properties
   if (customPiiProperties.hasOwnProperty(property)) {
     // hash or drop
     if (customPiiProperties[property] && typeof value === 'string') {
       payload[property] = sha256(value);
     }
     continue;
   }

   // Default PII properties
   var isPropertyPii = defaultPiiProperties.indexOf(property) >= 0;
   var isPropertyWhitelisted = whitelistPiiProperties.indexOf(property) >= 0;
   if (!isPropertyPii || isPropertyWhitelisted) {
     payload[property] = value;
   }
 }

 return payload;
};

/**
* Validates that a set of parameters are formatted correctly and passes them to the pixel instance.
* https://developers.facebook.com/docs/marketing-apis/data-processing-options#reference
*
* @param {Array} options
*
* @api private
*/
FacebookPixel.prototype.validateAndSetDataProcessing = function(params) {
 var lenOk = params.length === 3;
 var valOk =
   Array.isArray(params[0]) &&
   typeof params[1] === 'number' &&
   typeof params[2] === 'number';

 // Pass the data processing options if they're valid, otherwise, fallback to geolocation.
 if (lenOk && valOk) {
   window.fbq('dataProcessingOptions', params[0], params[1], params[2]);
 } else {
   window.fbq('dataProcessingOptions', ['LDU'], 0, 0);
 }
};

/**
* Merge two javascript objects. This works similarly to `Object.assign({}, obj1, obj2)`
* but it's compatible with old browsers. The properties of the first argument takes preference
* over the other.
*
* It does not do fancy stuff, just use it with top level properties.
*
* @param {Object} obj1 Object 1
* @param {Object} obj2 Object 2
*
* @return {Object} a new object with all the properties of obj1 and the remainder of obj2.
*/
function merge(obj1, obj2) {
 var res = {};

 // All properties of obj1
 for (var propObj1 in obj1) {
   if (obj1.hasOwnProperty(propObj1)) {
     res[propObj1] = obj1[propObj1];
   }
 }

 // Extra properties of obj2
 for (var propObj2 in obj2) {
   if (obj2.hasOwnProperty(propObj2) && !res.hasOwnProperty(propObj2)) {
     res[propObj2] = obj2[propObj2];
   }
 }

 return res;
}

// Exposed only for testing
FacebookPixel.merge = merge;

The Execution

Click a photo to enter the gallery

No items found.
No items found.

The Impact