Skip to content

Commit c99468a

Browse files
ccnixongpsamson
authored andcommitted
Add PII filtering (segmentio#62)
1 parent 54a8aba commit c99468a

File tree

5 files changed

+449
-325
lines changed

5 files changed

+449
-325
lines changed

.prettierrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"singleQuote": true
3+
}

integrations/.eslintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"extends": ["@segment/eslint-config/browser/legacy", "prettier"]
2+
"extends": ["@segment/eslint-config/browser/legacy", "@segment/eslint-config/mocha", "prettier"]
33
}

integrations/facebook-pixel/lib/index.js

+116-62
Original file line numberDiff line numberDiff line change
@@ -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() {
9495
FacebookPixel.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) {
218250
FacebookPixel.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) {
244281
FacebookPixel.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) {
270312
FacebookPixel.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
*/
311360
FacebookPixel.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({

integrations/facebook-pixel/test/.eslintrc

-3
This file was deleted.

0 commit comments

Comments
 (0)