@@ -17,18 +17,19 @@ var Track = require('segmentio-facade').Track;
17
17
* Expose `Facebook Pixel`.
18
18
*/
19
19
20
- var FacebookPixel = module . exports = integration ( 'Facebook Pixel' )
20
+ var FacebookPixel = ( module . exports = integration ( 'Facebook Pixel' )
21
21
. global ( 'fbq' )
22
22
. option ( 'pixelId' , '' )
23
23
. option ( 'agent' , 'seg' )
24
24
. option ( 'valueIdentifier' , 'value' )
25
25
. option ( 'initWithExistingTraits' , false )
26
26
. option ( 'traverse' , false )
27
27
. option ( 'automaticConfiguration' , true )
28
+ . option ( 'whitelistPiiProperties' , [ ] )
28
29
. mapping ( 'standardEvents' )
29
30
. mapping ( 'legacyEvents' )
30
31
. mapping ( 'contentTypes' )
31
- . tag ( '<script src="//connect.facebook.net/en_US/fbevents.js">' ) ;
32
+ . tag ( '<script src="//connect.facebook.net/en_US/fbevents.js">' ) ) ;
32
33
33
34
/**
34
35
* Initialize Facebook Pixel.
@@ -94,46 +95,74 @@ FacebookPixel.prototype.page = function() {
94
95
FacebookPixel . prototype . track = function ( track ) {
95
96
var event = track . event ( ) ;
96
97
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
+ }
102
105
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 ) {
130
157
return acc ;
131
158
}
132
- }
133
159
134
- acc [ key ] = val ;
135
- return acc ;
136
- } , { } , track . properties ( ) ) ;
160
+ acc [ key ] = val ;
161
+ return acc ;
162
+ } ,
163
+ { } ,
164
+ track . properties ( )
165
+ ) ;
137
166
138
167
var standard = this . standardEvents ( event ) ;
139
168
var legacy = this . legacyEvents ( event ) ;
@@ -174,7 +203,7 @@ FacebookPixel.prototype.productListViewed = function(track) {
174
203
var contentType ;
175
204
var contentIds = [ ] ;
176
205
var products = track . products ( ) ;
177
-
206
+
178
207
// First, check to see if a products array with productIds has been defined.
179
208
if ( Array . isArray ( products ) ) {
180
209
products . forEach ( function ( product ) {
@@ -196,7 +225,10 @@ FacebookPixel.prototype.productListViewed = function(track) {
196
225
197
226
window . fbq ( 'track' , 'ViewContent' , {
198
227
content_ids : contentIds ,
199
- content_type : this . mappedContentTypesOrDefault ( track . category ( ) , contentType ) ,
228
+ content_type : this . mappedContentTypesOrDefault (
229
+ track . category ( ) ,
230
+ contentType
231
+ )
200
232
} ) ;
201
233
202
234
// fall through for mapped legacy conversions
@@ -218,11 +250,16 @@ FacebookPixel.prototype.productListViewed = function(track) {
218
250
FacebookPixel . prototype . productViewed = function ( track ) {
219
251
window . fbq ( 'track' , 'ViewContent' , {
220
252
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
+ ] ) ,
222
256
content_name : track . name ( ) || '' ,
223
257
content_category : track . category ( ) || '' ,
224
258
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 ( ) )
226
263
} ) ;
227
264
228
265
// fall through for mapped legacy conversions
@@ -244,11 +281,16 @@ FacebookPixel.prototype.productViewed = function(track) {
244
281
FacebookPixel . prototype . productAdded = function ( track ) {
245
282
window . fbq ( 'track' , 'AddToCart' , {
246
283
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
+ ] ) ,
248
287
content_name : track . name ( ) || '' ,
249
288
content_category : track . category ( ) || '' ,
250
289
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 ( ) )
252
294
} ) ;
253
295
254
296
// fall through for mapped legacy conversions
@@ -270,20 +312,27 @@ FacebookPixel.prototype.productAdded = function(track) {
270
312
FacebookPixel . prototype . orderCompleted = function ( track ) {
271
313
var products = track . products ( ) || [ ] ;
272
314
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
+ ) ;
279
325
280
326
var revenue = formatRevenue ( track . revenue ( ) ) ;
281
327
282
328
// Order completed doesn't have a top-level category spec'd.
283
329
// Let's default to the category of the first product. - @gabriel
284
330
var contentType = [ 'product' ] ;
285
331
if ( products . length ) {
286
- contentType = this . mappedContentTypesOrDefault ( products [ 0 ] . category , contentType ) ;
332
+ contentType = this . mappedContentTypesOrDefault (
333
+ products [ 0 ] . category ,
334
+ contentType
335
+ ) ;
287
336
}
288
337
289
338
window . fbq ( 'track' , 'Purchase' , {
@@ -305,19 +354,19 @@ FacebookPixel.prototype.orderCompleted = function(track) {
305
354
/**
306
355
* mappedContentTypesOrDefault returns an array of mapped content types for
307
356
* 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
310
359
*/
311
360
FacebookPixel . prototype . mappedContentTypesOrDefault = function ( category , def ) {
312
- if ( ! category ) return def
361
+ if ( ! category ) return def ;
313
362
314
363
var mapped = this . contentTypes ( category ) ;
315
364
if ( mapped . length ) {
316
- return mapped
317
- }
318
-
319
- return def
320
- }
365
+ return mapped ;
366
+ }
367
+
368
+ return def ;
369
+ } ;
321
370
322
371
/**
323
372
* Get Revenue Formatted Correctly for FB.
@@ -349,17 +398,22 @@ function formatTraits(analytics) {
349
398
firstName = traits . firstName ;
350
399
lastName = traits . lastName ;
351
400
} else {
352
- var nameArray = traits . name && traits . name . toLowerCase ( ) . split ( ' ' ) || [ ] ;
401
+ var nameArray = ( traits . name && traits . name . toLowerCase ( ) . split ( ' ' ) ) || [ ] ;
353
402
firstName = nameArray . shift ( ) ;
354
403
lastName = nameArray . pop ( ) ;
355
404
}
356
405
var gender ;
357
406
if ( traits . gender && is . string ( traits . gender ) ) {
358
- gender = traits . gender . slice ( 0 , 1 ) . toLowerCase ( ) ;
407
+ gender = traits . gender . slice ( 0 , 1 ) . toLowerCase ( ) ;
359
408
}
360
409
var birthday = traits . birthday && dateformat ( traits . birthday , 'yyyymmdd' ) ;
361
410
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 ( ) ;
363
417
var state = address . state && address . state . toLowerCase ( ) ;
364
418
var postalCode = address . postalCode ;
365
419
return reject ( {
0 commit comments