@@ -17,18 +17,19 @@ var Track = require('segmentio-facade').Track;
1717 * Expose `Facebook Pixel`.
1818 */
1919
20- var FacebookPixel = module . exports = integration ( 'Facebook Pixel' )
20+ var FacebookPixel = ( module . exports = integration ( 'Facebook Pixel' )
2121 . global ( 'fbq' )
2222 . option ( 'pixelId' , '' )
2323 . option ( 'agent' , 'seg' )
2424 . option ( 'valueIdentifier' , 'value' )
2525 . option ( 'initWithExistingTraits' , false )
2626 . option ( 'traverse' , false )
2727 . option ( 'automaticConfiguration' , true )
28+ . option ( 'whitelistPiiProperties' , [ ] )
2829 . mapping ( 'standardEvents' )
2930 . mapping ( 'legacyEvents' )
3031 . mapping ( 'contentTypes' )
31- . tag ( '<script src="//connect.facebook.net/en_US/fbevents.js">' ) ;
32+ . tag ( '<script src="//connect.facebook.net/en_US/fbevents.js">' ) ) ;
3233
3334/**
3435 * Initialize Facebook Pixel.
@@ -94,46 +95,74 @@ FacebookPixel.prototype.page = function() {
9495FacebookPixel . prototype . track = function ( track ) {
9596 var event = track . event ( ) ;
9697 var revenue = formatRevenue ( track . revenue ( ) ) ;
97- var payload = foldl ( function ( acc , val , key ) {
98- if ( key === 'revenue' ) {
99- acc . value = revenue ;
100- return acc ;
101- }
98+ var whitelistPiiProperties = this . options . whitelistPiiProperties || [ ] ;
99+ var payload = foldl (
100+ function ( acc , val , key ) {
101+ if ( key === 'revenue' ) {
102+ acc . value = revenue ;
103+ return acc ;
104+ }
102105
103- /**
104- * FB requires these date fields be formatted in a specific way.
105- * The specifications are non iso8601 compliant.
106- * https://developers.facebook.com/docs/marketing-api/dynamic-ads-for-travel/audience
107- * Therefore, we check if the property is one of these reserved fields.
108- * If so, we check if we have converted it to an iso date object already.
109- * If we have, we convert it again into Facebook's spec.
110- * If we have not, the user has likely passed in a date string that already
111- * adheres to FB's docs so we can just pass it through as is.
112- * @ccnixon
113- */
114-
115- var dateFields = [
116- 'checkinDate' ,
117- 'checkoutDate' ,
118- 'departingArrivalDate' ,
119- 'departingDepartureDate' ,
120- 'returningArrivalDate' ,
121- 'returningDepartureDate' ,
122- 'travelEnd' ,
123- 'travelStart'
124- ] ;
125-
126- if ( dateFields . indexOf ( camel ( key ) ) >= 0 ) {
127- if ( is . date ( val ) ) {
128- val = val . toISOString ( ) . split ( 'T' ) [ 0 ] ;
129- acc [ key ] = val ;
106+ /**
107+ * FB requires these date fields be formatted in a specific way.
108+ * The specifications are non iso8601 compliant.
109+ * https://developers.facebook.com/docs/marketing-api/dynamic-ads-for-travel/audience
110+ * Therefore, we check if the property is one of these reserved fields.
111+ * If so, we check if we have converted it to an iso date object already.
112+ * If we have, we convert it again into Facebook's spec.
113+ * If we have not, the user has likely passed in a date string that already
114+ * adheres to FB's docs so we can just pass it through as is.
115+ * @ccnixon
116+ */
117+
118+ var dateFields = [
119+ 'checkinDate' ,
120+ 'checkoutDate' ,
121+ 'departingArrivalDate' ,
122+ 'departingDepartureDate' ,
123+ 'returningArrivalDate' ,
124+ 'returningDepartureDate' ,
125+ 'travelEnd' ,
126+ 'travelStart'
127+ ] ;
128+
129+ if ( dateFields . indexOf ( camel ( key ) ) >= 0 ) {
130+ if ( is . date ( val ) ) {
131+ val = val . toISOString ( ) . split ( 'T' ) [ 0 ] ;
132+ acc [ key ] = val ;
133+ return acc ;
134+ }
135+ }
136+
137+ // FB does not allow sending PII data with events. They provide a list of what they consider PII here:
138+ // https://developers.facebook.com/docs/facebook-pixel/pixel-with-ads/conversion-tracking
139+ // 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.
140+ // User's can override this by manually whitelisting keys they are ok with sending through in their integration settings.
141+
142+ var pii = [
143+ 'email' ,
144+ 'firstName' ,
145+ 'lastName' ,
146+ 'gender' ,
147+ 'city' ,
148+ 'country' ,
149+ 'phone' ,
150+ 'state' ,
151+ 'zip' ,
152+ 'birthday'
153+ ] ;
154+
155+ var propertyWhitelisted = whitelistPiiProperties . indexOf ( key ) >= 0 ;
156+ if ( pii . indexOf ( key ) >= 0 && ! propertyWhitelisted ) {
130157 return acc ;
131158 }
132- }
133159
134- acc [ key ] = val ;
135- return acc ;
136- } , { } , track . properties ( ) ) ;
160+ acc [ key ] = val ;
161+ return acc ;
162+ } ,
163+ { } ,
164+ track . properties ( )
165+ ) ;
137166
138167 var standard = this . standardEvents ( event ) ;
139168 var legacy = this . legacyEvents ( event ) ;
@@ -174,7 +203,7 @@ FacebookPixel.prototype.productListViewed = function(track) {
174203 var contentType ;
175204 var contentIds = [ ] ;
176205 var products = track . products ( ) ;
177-
206+
178207 // First, check to see if a products array with productIds has been defined.
179208 if ( Array . isArray ( products ) ) {
180209 products . forEach ( function ( product ) {
@@ -196,7 +225,10 @@ FacebookPixel.prototype.productListViewed = function(track) {
196225
197226 window . fbq ( 'track' , 'ViewContent' , {
198227 content_ids : contentIds ,
199- content_type : this . mappedContentTypesOrDefault ( track . category ( ) , contentType ) ,
228+ content_type : this . mappedContentTypesOrDefault (
229+ track . category ( ) ,
230+ contentType
231+ )
200232 } ) ;
201233
202234 // fall through for mapped legacy conversions
@@ -218,11 +250,16 @@ FacebookPixel.prototype.productListViewed = function(track) {
218250FacebookPixel . prototype . productViewed = function ( track ) {
219251 window . fbq ( 'track' , 'ViewContent' , {
220252 content_ids : [ track . productId ( ) || track . id ( ) || track . sku ( ) || '' ] ,
221- content_type : this . mappedContentTypesOrDefault ( track . category ( ) , [ 'product' ] ) ,
253+ content_type : this . mappedContentTypesOrDefault ( track . category ( ) , [
254+ 'product'
255+ ] ) ,
222256 content_name : track . name ( ) || '' ,
223257 content_category : track . category ( ) || '' ,
224258 currency : track . currency ( ) ,
225- value : this . options . valueIdentifier === 'value' ? formatRevenue ( track . value ( ) ) : formatRevenue ( track . price ( ) )
259+ value :
260+ this . options . valueIdentifier === 'value'
261+ ? formatRevenue ( track . value ( ) )
262+ : formatRevenue ( track . price ( ) )
226263 } ) ;
227264
228265 // fall through for mapped legacy conversions
@@ -244,11 +281,16 @@ FacebookPixel.prototype.productViewed = function(track) {
244281FacebookPixel . prototype . productAdded = function ( track ) {
245282 window . fbq ( 'track' , 'AddToCart' , {
246283 content_ids : [ track . productId ( ) || track . id ( ) || track . sku ( ) || '' ] ,
247- content_type : this . mappedContentTypesOrDefault ( track . category ( ) , [ 'product' ] ) ,
284+ content_type : this . mappedContentTypesOrDefault ( track . category ( ) , [
285+ 'product'
286+ ] ) ,
248287 content_name : track . name ( ) || '' ,
249288 content_category : track . category ( ) || '' ,
250289 currency : track . currency ( ) ,
251- value : this . options . valueIdentifier === 'value' ? formatRevenue ( track . value ( ) ) : formatRevenue ( track . price ( ) )
290+ value :
291+ this . options . valueIdentifier === 'value'
292+ ? formatRevenue ( track . value ( ) )
293+ : formatRevenue ( track . price ( ) )
252294 } ) ;
253295
254296 // fall through for mapped legacy conversions
@@ -270,20 +312,27 @@ FacebookPixel.prototype.productAdded = function(track) {
270312FacebookPixel . prototype . orderCompleted = function ( track ) {
271313 var products = track . products ( ) || [ ] ;
272314
273- var content_ids = foldl ( function ( acc , product ) {
274- var item = new Track ( { properties : product } ) ;
275- var key = item . productId ( ) || item . id ( ) || item . sku ( ) ;
276- if ( key ) acc . push ( key ) ;
277- return acc ;
278- } , [ ] , products ) ;
315+ var content_ids = foldl (
316+ function ( acc , product ) {
317+ var item = new Track ( { properties : product } ) ;
318+ var key = item . productId ( ) || item . id ( ) || item . sku ( ) ;
319+ if ( key ) acc . push ( key ) ;
320+ return acc ;
321+ } ,
322+ [ ] ,
323+ products
324+ ) ;
279325
280326 var revenue = formatRevenue ( track . revenue ( ) ) ;
281327
282328 // Order completed doesn't have a top-level category spec'd.
283329 // Let's default to the category of the first product. - @gabriel
284330 var contentType = [ 'product' ] ;
285331 if ( products . length ) {
286- contentType = this . mappedContentTypesOrDefault ( products [ 0 ] . category , contentType ) ;
332+ contentType = this . mappedContentTypesOrDefault (
333+ products [ 0 ] . category ,
334+ contentType
335+ ) ;
287336 }
288337
289338 window . fbq ( 'track' , 'Purchase' , {
@@ -305,19 +354,19 @@ FacebookPixel.prototype.orderCompleted = function(track) {
305354/**
306355 * mappedContentTypesOrDefault returns an array of mapped content types for
307356 * the category - or returns the defaul value.
308- * @param {Facade.Track } track
309- * @param {Array } def
357+ * @param {Facade.Track } track
358+ * @param {Array } def
310359 */
311360FacebookPixel . prototype . mappedContentTypesOrDefault = function ( category , def ) {
312- if ( ! category ) return def
361+ if ( ! category ) return def ;
313362
314363 var mapped = this . contentTypes ( category ) ;
315364 if ( mapped . length ) {
316- return mapped
317- }
318-
319- return def
320- }
365+ return mapped ;
366+ }
367+
368+ return def ;
369+ } ;
321370
322371/**
323372 * Get Revenue Formatted Correctly for FB.
@@ -349,17 +398,22 @@ function formatTraits(analytics) {
349398 firstName = traits . firstName ;
350399 lastName = traits . lastName ;
351400 } else {
352- var nameArray = traits . name && traits . name . toLowerCase ( ) . split ( ' ' ) || [ ] ;
401+ var nameArray = ( traits . name && traits . name . toLowerCase ( ) . split ( ' ' ) ) || [ ] ;
353402 firstName = nameArray . shift ( ) ;
354403 lastName = nameArray . pop ( ) ;
355404 }
356405 var gender ;
357406 if ( traits . gender && is . string ( traits . gender ) ) {
358- gender = traits . gender . slice ( 0 , 1 ) . toLowerCase ( ) ;
407+ gender = traits . gender . slice ( 0 , 1 ) . toLowerCase ( ) ;
359408 }
360409 var birthday = traits . birthday && dateformat ( traits . birthday , 'yyyymmdd' ) ;
361410 var address = traits . address || { } ;
362- var city = address . city && address . city . split ( ' ' ) . join ( '' ) . toLowerCase ( ) ;
411+ var city =
412+ address . city &&
413+ address . city
414+ . split ( ' ' )
415+ . join ( '' )
416+ . toLowerCase ( ) ;
363417 var state = address . state && address . state . toLowerCase ( ) ;
364418 var postalCode = address . postalCode ;
365419 return reject ( {
0 commit comments