How we protect your ticketing information from third-party trackers
From the very beginning, privacy has been one of the most important design goals of pretix. Privacy is usually best and easiest achieved by just not collecting any unnecessary data that and we give our very best to live by that standard.
By default, visiting a pretix shop does not create a single request to a third-party domain and does not transmit any of your data to someone other than us, the ticket seller, and in some cases the provider handling the payment.
The problem
Some of our customers do want to include third-party trackers from Google Analytics, Facebook, LinkedIn, or other platforms in their ticket shop to measure conversion of their marketing campaigns. Of course, we want to enable them to run powerful marketing campaigns to sell their tickets, so we optionally allow turning on these tracking providers.
In this case the tracking company (like Google or Facebook) will get a request from your browser and know things like your IP adress and details on your browser session. This might be enough to personally identify you, especially when you’re logged in to Facebook, but there isn’t much we can do about that.
However, since most of these analytics are about statistical data on how well a certain marketing campaign performs, not about person-specific data, data is usually anonymized or pseudonymized on the other end to comply with regulations. For example, the last byte of an IP address might be stripped off or session data is only stored/presented as a hashed value. This allows to create profiles of users for advertising, but doesn’t link those profiles to actual persons.
One of the data points that all of these trackers store is the page you are currently
visiting. That’s understandable, since ticket sellers want to know what events people are looking
at or how conversion drops throughout the checkout process. However, after you bought a ticket,
pretix will send you to an URL specific to your order, e.g. https://pretix.eu/demo/democon/order/XR9HT/M74wWOKiM9SxmbNi/
.
If you don’t see the problem yet, this means that your browsing profile as seen from the tracker will no longer by anonymous in any regard: It can now be easily linked back to your order and from there to your name, email address, potentially banking details and much more.
Additionally, this is a potential security issue: A data leak in the tracking system (or on the way there) would make it very easy to gain access to valuable attendee data – and tickets to the event.
Naturally, we want to hide that information from trackers.
Our solution
The only tracker providing an official solution here is Google Analytics. Given a function
clean_url()
that removes sensitive data from the URL, it’s just a simple call to the Google
Analytics JavaScript API to fix:
ga('set', 'location', clean_url(document.location.toString()));
Unfortunately, none of the other tracking providers have an official way to do this, so we need to trick them. In the past, we just vendored the tracking scripts of those providers and changed them to send different URLs, but that approach was flawed for two reasons: First, it requires a lot of effort to integrate any new provider, and second, it requires us to monitor changes to the tracking scripts of all providers to stay compatible.
Clearly, we needed a better solution. Since those JavaScript snippets get the sensitive information
that we want to protect from one of the three global variables window.location
, document.location
, or location
, we need to somehow manipulate this information before they can fetch it.
However, you cannot just re-assign those objects: If you write window.location = '/cleaned/path'
, your browser will instantly navigate you there! But even if that wasn’t the case, it would be nicer to have a solution that doesn’t have impact on our own scripts that might need to access the location object for legitimate reasons.
In the end, we implemented a proxy that automatically fetches the JavaScript snippet from the tracking provider, wraps it in some code we provide and then provides it to you. This wrapping code will put the third-party JavaScript within its own variable scope and will override the sensitive information within that scope.
Overriding window
and document
is a bit delicate, since the wrapped script use these
objects for all kinds of things, not just accessing the location, so we need to proxy lots of
properties of these objects through – just not the ones we want to filter first. In the end,
the wrapping script looks something like this:
(function () {
// Store a reference to the actual objects
var _location = window.location;
var _window = window;
var _document = document;
function _clean_url(url) {
url = url.replace(
/^(.*)\/order\/([A-Z0-9]+)\/([A-Z0-9]+)\/(.*)$/gi,
"$1/order/CODE/secret/$4"
);
// Perform cleanup of more sensitive parameters
return url;
}
(function () {
// Build a filtered version of the location object
var _clean_location = {
'href': _clean_url(_location.href),
'toString': function () {
return _clean_url(_location.toString());
},
'replace': function () {
console.log("Changing location blocked.");
return false;
},
// … continue for all the other properties
// of window.location
};
// Shadow global variables. Any code in this scope will
// see these objects instead of the actual window object.
var location = _clean_location;
var window = {};
Object.defineProperty(window, 'location', {
get: function () { return _clean_location; },
set: function () { return false; }
});
// Redefine important functions and properties on these
// objects to keep the scripts working
window.addEventListener = function (a,b,c,d) {
return _window.addEventListener(a,b,c,d);
}
var a = [
"performance", "chrome", "navigator", "top",
// … continue with all other important
// properties of window
];
for (var x in a) {
(function() {
var n = a[x];
Object.defineProperty(window, n, {
get: function () { return _window[n]; },
set: function (val) { _window[n] = val; }
});
})();
}
window.__proto__ = _window;
// Shadow global "document" variable in the same way
// (not shown here for simplicity)
// Think about document.referrer as well
// Insert original tracker code here
}());
}());
This way, the third-party tracker will only see a filtered version of the URL and cannot use this data point to link your browsing profile to a specific ticket order. The secret link to your ticket order will no longer be transmitted to any third-party entity!
Some special treatment is still required for some trackers who try to store variables in global scope to access them from different scripts later, but so far we’ve managed to integrated all supported trackers this way.
P.S: Every shop hosted on our infrastructure, pretix.eu, has a “Privacy” link at the very bottom of the page, leading to an individual privacy information for this shop. On this page, we list all trackers enabled on this shop specifically and allow you to opt out of being tracked by any of them. If you opt out, we won’t even ship their JavaScript code to your browser.